5
头图

八. 测试 react 代码

54. 使用 React 测试库有效地测试你的 React 组件

想要测试你的 React 应用吗?请务必使用 @testing-library/react

你可以在此处找到一个最基本的示例

55. React 测试库:使用测试演练场轻松创建测试用例

难以决定在测试中使用哪些测试用例?

考虑使用测试演练场从组件的 HTML 快速生成测试用例。

以下是两种使用方法:

方法 1:在测试中使用 screen.logTestingPlaygroundURL()。此函数生成一个 URL,打开测试环境工具,其中已加载组件的 HTML。

方法 2:安装 Testing Playground Chrome 扩展程序。此扩展程序允许你直接在浏览器中将鼠标悬停在应用中的元素上,以找到测试它们的最佳查询。

56. 使用 Cypress 或 Playwright 进行端到端测试

需要进行端到端测试吗?

请务必查看 CypressPlaywright

57. 使用 MSW 在测试中模拟网络请求

有时,你的测试需要发出网络请求。

与其实现自己的模拟(或者,但愿不会发出实际的网络请求),不如考虑使用 MSW(Mock Service Worker)来处理你的 API 响应。

MSW 允许你直接在测试中拦截和操纵网络交互,为模拟服务器响应提供了一种强大而直接的解决方案,而不会影响实时服务器。

这种方法有助于维护受控且可预测的测试环境,从而提高测试的可靠性。

九. React hooks(钩子函数)

58. 确保在 useEffect 钩子中执行所有必要的清理

如果你设置了任何需要稍后清理的内容,请始终在 useEffect 钩子中返回清理函数,这可能是任何内容,忽略此步骤可能会导致资源使用率低下和潜在的内存泄漏。

不好的做法:此示例设置了一个间隔。但我们从未清除它,这意味着即使组件卸载后它仍会继续运行。

const Timer = () => {
  const [date, setDate] = useState(new Date());
  useEffec(() => {
    setInterval(() => {
      setDate(new Date());
    }, 1000);
  }, []);
  return <>当前时间:{date.toLocaleTimeString()}</>;
};

推荐做法: 当组件卸载时,间隔会被正确清除。

const Timer = () => {
  const [date, setDate] = useState(new Date());
  useEffec(() => {
    const interval = setInterval(() => {
      setDate(new Date());
    }, 1000);
    // 当组件卸载时,我们清除了定时器
    return () => clearInterval(interval);
  }, []);
  return <>当前时间:{date.toLocaleTimeString()}</>;
};

59. 使用 ref 访问 DOM 元素

在 React 中,你永远不应该直接操作 DOM。

尽管 React 可以直接访问/操作 DOM,不过还是不推荐使用 document.getElementById document.getElementsByClassName 等方法。

那么,当你需要访问 DOM 元素时应该怎么做?

你可以使用 useRef 钩子函数,如下面的示例中所示,我们需要访问 canvas 元素。

import { useEffect, useRef } from "react";
import Chart from "chart.js/auto";

export interface ChartComponentProps<T> {
  data?: T[];
}
const ChartComponent = <T extends { year?: number; count?: number }>({
  data,
}: ChartComponentProps<T>) => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  useEffect(() => {
    const canvasElement = canvasRef.current;
    if (canvasElement == null) {
      return;
    }
    const chart = new Chart(canvasElement, {
      type: "bar",
      data: {
        labels: data?.map((row) => row.year),
        datasets: [
          {
            label: "一年的点赞数",
            data: data?.map((row) => row.count),
          },
        ],
      },
    });
    return () => chart.destroy();
  }, []);
  return <canvas ref={canvasRef} />;
};

export default ChartComponent;
注意:我们可以向 canvas 元素添加一个 ID 并使用 document.getElementById 获取,但不建议这样做。

60. 使用 ref 在重新渲染时保存值

如果你的 React 组件中有未存储在状态中的可变值,你会注意到对这些值的更改不会在重新渲染后持续存在。

除非你全局保存它们,否则会发生这种情况。

你可能会考虑将这些值放入状态中。但是,如果它们与渲染无关,这样做可能会导致不必要的重新渲染,从而浪费性能。

这也是 useRef 大放异彩的地方。

在下面的例子中,我想在用户点击某个按钮时停止计时器。为此,我需要将 interval 存储在某处。

不好的做法:下面的示例无法按预期工作,因为每次重新渲染组件时都会重置 interval。

const Timer = () => {
  const [date, setDate] = useState(new Date());
  let interval: ReturnType<typeof setInterval>;
  useEffec(() => {
    interval = setInterval(() => {
      setDate(new Date());
    }, 1000);
    // 当组件卸载时,我们清除了定时器
    return () => clearInterval(interval);
  }, []);
  const stopInterval = () => interval && clearInterval(interval);
  return (
    <>
      <p>当前时间:{date.toLocaleTimeString()}</p>
      <button onClick={stopInterval} type="button">
        停止定时器
      </button>
    </>
  );
};

推荐做法: 通过使用 useRef,我们确保渲染之间的间隔 ID 得以保留。

const Timer = () => {
  const [date, setDate] = useState(new Date());
  let interval = useRef<ReturnType<typeof setInterval>>();
  useEffec(() => {
    interval.current = setInterval(() => {
      setDate(new Date());
    }, 1000);
    // 当组件卸载时,我们清除了定时器
    return () => clearInterval(interval.current);
  }, []);
  const stopInterval = () =>
    interval.current && clearInterval(interval.current);
  return (
    <>
      <p>当前时间:{date.toLocaleTimeString()}</p>
      <button onClick={stopInterval} type="button">
        停止定时器
      </button>
    </>
  );
};

61. 在 hooks 中使用命名函数而不是箭头函数(例如 useEffect),以便在 React Dev Tools 中轻松找到它们

如果你有许多钩子函数,在 React DevTools 中找到它们可能会很困难。

一个技巧是使用命名函数,这样你就可以快速发现它们。

不好的做法: 在众多的钩子函数中很难找到具体的效果。

const HelloWorld = () => {
  useEffect(() => {
    console.log("我已经挂载了!");
  }, []);

  return <>Hello World</>;
};

推荐做法: 你可以很快发现其效果。

const HelloWorld = () => {
  useEffect(function HelloWorldFn() {
    console.log("我已经挂载了!");
  }, []);

  return <>Hello World</>;
};

62. 使用自定义钩子函数封装逻辑

假设我有一个组件,它从用户的暗模式偏好设置中获取主题并在应用程序内使用它。

最好将返回主题的逻辑提取到自定义钩子中(以重复使用它并保持组件清洁)。

不好的做法: App 组件过于繁琐。

const App = () => {
  const [theme, setTheme] = useState("light");

  useEffect(() => {
    const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    setTheme(dqMediaQuery.matches ? "dark" : "light");
    const listener = (event) => {
      setTheme(event.matches ? "dark" : "light");
    };
    dqMediaQuery.addEventListener("change", listener);
    return () => {
      dqMediaQuery.removeEventListener("change", listener);
    };
  }, []);

  return (
    <div className={`App ${theme === "dark" ? "dark" : ""}`}>Hello Word</div>
  );
};

推荐做法: App 组件简单多了,我们可以重用逻辑。

// 自定义钩子函数可以被重复使用
const useTheme = () => {
  const [theme, setTheme] = useState("light");

  useEffect(() => {
    const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    setTheme(dqMediaQuery.matches ? "dark" : "light");
    const listener = (event) => {
      setTheme(event.matches ? "dark" : "light");
    };
    dqMediaQuery.addEventListener("change", listener);
    return () => {
      dqMediaQuery.removeEventListener("change", listener);
    };
  }, []);

  return theme;
};
const App = () => {
  const theme = useTheme();
  return (
    <div className={`App ${theme === "dark" ? "dark" : ""}`}>Hello Word</div>
  );
};

63. 优先使用函数而不是自定义钩子函数

当可以使用函数时,切勿将逻辑放在钩子函数中。

效果:

  • 钩子函数只能在其他钩子函数或组件内使用,而函数可以在任何地方使用。
  • 函数比钩子函数更简单。
  • 函数更容易测试。
  • 等等。

不好的做法: useLocale 钩子是不必要的,因为它不需要是一个钩子。它不使用其他钩子,如 useEffectuseState 等。

const useLocale = () => {
  return window.navigator.languages?.[0] ?? window.navigator.language;
};
const App = () => {
  const locale = useLocale();
  return (
    <div className="App">
      <ConfigProvider locale={locale}>
        <Main />
      </ConfigProvider>
    </div>
  );
};

推荐做法: 创建一个函数 getLocale

const getLocale = () =>
  window.navigator.languages?.[0] ?? window.navigator.language;
const App = () => {
  const locale = getLocale();
  return (
    <div className="App">
      <ConfigProvider locale={locale}>
        <Main />
      </ConfigProvider>
    </div>
  );
};

64. 使用 useLayoutEffect 钩子防止视觉 UI 故障

当效果不是由用户交互引起时,用户将在效果运行之前看到 UI(通常很短暂)。

因此,如果效果修改了 UI,用户将在看到更新后的 UI 版本之前很快看到初始 UI 版本,从而产生视觉故障。

使用 useLayoutEffect 可确保效果在所有 DOM 突变后同步运行,从而防止初始渲染故障。

在下面的示例中,我们希望宽度在列之间均匀分布(我知道这可以在 CSS 中完成,但我需要一个例子)。

使用 useEffect,你可以在开始时短暂地看到表格正在发生变化。列以其默认大小呈现,然后调整为正确大小。

const BlogPostsTable = ({ posts }: { posts: BlogPosts }) => {
  const tableRef = useRef<HTMLTableElement | null>(null);
  const [columnWidth, setColumnWidth] = useState<number>();

  // 使用 `useLayoutEffect` 来查看表格如何以正确的尺寸呈现
  useEffect(() => {
    // 屏幕故障太快,可能不可见
    // 所以我只是挡住屏幕让故障可见
    blockScreenSync();
    const tableElement = tableRef.current;
    if (tableElement != null) {
      // 在列之间平均分配宽度
      // 这可以用 CSS 来实现,所以我们在这里这样做是为了说明目的
      setColumnWidth(tableElement.offsetWidth / 4);
    }
  }, []);

  return (
    <table ref={tableRef}>
      <thead>
        <tr>
          {tableColumn.map((item, index) => (
            <th key={`${item}-${index}`}>{item.title}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {posts.map((post) => (
          <tr key={post.href}>
            {tableColumn.map((col, index) => (
              <td key={`${col}-${index}`} style={{ width: columnWidth }}>
                {col.render ? (
                  col.render(post[col.dataIndex as keyof BlogPostsItem] as any)
                ) : (
                  <>{post[col.dataIndex as keyof BlogPostsItem]}</>
                )}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
};
如果你正在寻找其他出色的用途,请查看这篇文章

65. 使用 useId 钩子为可访问性属性生成唯一 ID

厌倦了想出 ID 或让它们发生冲突?

你可以使用 useId 钩子函数在 React 组件内生成唯一 ID,并确保你的应用可访问。

示例如下:

const TestForm = () => {
  const id = useId();
  return (
    <div className="App">
      <div className="form-item">
        <label>用户名:</label>
        <input type="text" aria-describedby={id} placeholder="请输入用户名" />
      </div>
      <span id={id}>确保用户名是不重复的</span>
    </div>
  );
};

66. 使用 useSyncExternalStore 订阅外部存储

这是一个很少需要但功能非常强大的钩子。

如果出现以下情况,请使用此钩子:

  • 你有一些在 React 树中无法访问的状态(即,在状态或上下文中不存在)。
  • 状态可以更改,并且你需要通知你的组件更改。

在下面的示例中,我想要一个 Logger 单例来记录整个应用程序中的错误、警告、信息等。

这些是需求:

  • 我需要能够在我的 React 应用程序中的任何地方调用它(即使在非 React 组件内),所以我不会将它放在状态/上下文中。
  • 我想在 Logs 组件内向用户显示所有日志。

我可以在 Logs 组件内使用 useSyncExternalStore 来访问日志并监听更改。

const createLogger = <
  T extends { level: string; message: string },
  U extends () => void
>() => {
  let logDataList: T[] = [],
    logListeners: U[] = [];
  const pushLog = (log: T) => {
    logDataList = [...logDataList, log];
    logListeners.forEach((listener) => listener());
  };

  return {
    logs: () => Object.freeze(logDataList),
    subscribe: (listener: U) => {
      logListeners.push(listener);
      return () => {
        logListeners = logListeners.filter((l) => l !== listener);
      };
    },
    info: (message: string) => {
        pushLog({ level: "info", message } as T);
        console.info(message);
    },
    error: (message: string) => {
      pushLog({ level: "error", message } as T);
      console.error(message);
    },
    warn: (message: string) => {
      pushLog({ level: "warn", message } as T);
      console.warn(message);
    },
  };
};

前往这里查看完整的示例。

67. 使用 useDeferredValue 钩子显示先前的查询结果,直到有新的结果可用

假设你正在构建一个在地图上表示国家/地区的应用程序。

用户可以过滤以查看人口规模达到特定水平的国家/地区。

每次 maxPopulationSize 更新时,地图都会重新渲染(请参阅下面的示例)。

示例地址

因此,请注意滑块移动速度过快时滑块会变得多么不稳定。这是因为每次滑块移动时都会重新渲染地图。

为了解决这个问题,我们可以使用 useDeferredValue 钩子,以便滑块顺利更新。

const deferredMaxPopulationSize = useDeferredValue(maxPopulationSize);
<Map
    maxPopulationSize={deferredMaxPopulationSize}
    // …
/>

如果你正在寻找其他用法,请查看这篇文章

十. 必须知道的 React 库/工具

68. 使用 react-router 将路由功能集成到你的应用中

如果你需要你的应用支持多个页面,请查看 react-router

你可以在此处找到一个最简单的示例

69. 使用 swr 或 React Query 在你的应用中实现一流的数据获取

数据获取可能非常棘手。

但是,swrReact Query 等库可以让它变得容易得多。

对于简单的用例,建议使用 swr,对于更复杂的用例,建议使用 React Query

70. 使用 formik、React Hook Form 或 TanStack Form 等库简化表单状态管理

如果你在使用表单时遇到困难,推荐可以看看这些库。

71. 使用 Format.js、Lingui 或 react-i18next 使你的应用国际化。

如果你的应用需要支持多种语言,则应将其国际化。

你可以使用以下库来实现此目的:

72. 使用 framer-motion 轻松创建令人印象深刻的动画

动画可以让你的应用脱颖而出,你可以使用 framer-motion 轻松创建动画。

73. 还在使用自定义钩子重新发明轮子?

你是否还在使用自定义钩子来重新创造轮子?

推荐先看看ahooksusehooks,看看是否有人已经为你完成了这项工作。

74. 利用 UI 库简化应用程序开发

构建可访问、响应迅速且美观的大规模 UI 非常困难。

ShadcdnHeadless UIacro designant design、 []()等库可让这一过程变得更容易。

  • Shadcdn 提供了一组可访问、可重复使用且可组合的 React 组件,你可以将其复制并粘贴到你的应用中,不过它可能需要 Tailwind CSS
  • Headless UI 提供了无样式、完全可访问的 UI 组件,你可以使用它们来构建自己的 UI 组件。
  • acro design 提供了提供了一套全面的设计规范和组件库,确保设计一致性。组件库丰富,涵盖表单、表格、导航、图标等常用元素。支持灵活的主题定制和国际化。遵循响应式设计理念,考虑访问性标准。
  • Ant Design提供了丰富的组件体系,覆盖了常见的中后台应用场景,如通用组件、布局组件、导航组件和数据录入组件等。

75. 使用 axe-core-npm 库检查你的网站的可访问性

网站应该对所有人开放。

然而,很容易忽略可访问性问题。

axe-core-npm 是一种快速、安全且可靠的方法,可在开发网站时检查网站的可访问性。

提示:如果你是 VSCode 用户,则可以安装相关扩展:axe Accessibility Linter

76. 使用 react-codemod 轻松重构 React 代码

Codemods 是以编程方式在代码库上运行的转换,它们使重构代码库变得容易。

例如,React codemods 可以帮助你从代码库中删除所有 React 导入,更新代码以使用最新的 React 功能等等。

因此,在手动重构代码之前,请务必检查这些内容。

77. 使用 vite-pwa 将你的应用转变为渐进式 Web 应用程序 (PWA)

渐进式 Web 应用程序 (PWA) 的加载方式与常规网页类似,但提供离线工作、推送通知和设备硬件访问等功能。

你可以使用 vite-pwa 在 React 中轻松创建 PWA。

十一. React 与 Visual Studio Code

78. 使用 Simple React Snippets 代码片段扩展来提高你的工作效率

引导新的 React 组件可能很繁琐。

Simple React Snippets 扩展中的代码片段让这一切变得更容易。

79. 将 editor.stickyScroll.enabled 设置为 true,可以快速定位当前组件

如果文件很大,可能很难找到当前组件,通过将 editor.stickyScroll.enabled 设置为 true,当前组件将始终位于屏幕顶部。类似于吸附效果。

如下图所示:

80. 使用 VSCode Glean 或 VSCode React Refactor 等扩展简化重构

如果你需要频繁重构代码(例如,将 JSX 提取到新组件中),请务必查看 VSCode GleanVSCode React Refactor 等扩展。

十二. React 与 TypeScript

81. 使用 ReactNode 代替 JSX.Element | null | undefined | ... 来保持代码更简洁

不要像这样输入 leftElement 和 rightElement 属性:

type Element =  JSX.Element | null | undefined; // | ...
const Panel = ({ leftElement,rightElement }: { leftElement?: Element;rightElement?: Element }) => {
    // ...
}

你可以使用 ReactNode 来保持代码更简洁。

const Panel = ({ leftElement,rightElement }: { leftElement?: ReactNode;rightElement?: ReactNode }) => {
    // ...
}

82. 使用 PropsWithChildren 简化需要子 props 的组件的输入

你不必手动输入 children 属性。

事实上,你可以使用 PropsWithChildren 来简化输入。

// PropsWithChildren类型来自于react
import { PropsWithChildren } from 'react';

interface PageProps {
   // ...
}
// 这样做也没有什么问题
const HeaderPage = ({ children,...pageProps }: { children: ReactNode } & PageProps) => {
  // ...
};

// 更好的做法
const HeaderPage = ({ children, ...pageProps } : PropsWithChildren<PageProps>) => {
  // ...
};

83. 使用 ComponentProps、ComponentPropsWithoutRef 等高效访问元素的props

在某些情况下,你需要弄清楚组件的 props。

例如,假设你想要一个按钮,当单击时会记录到控制台。

你可以使用 ComponentProps 访问按钮元素的 props,然后覆盖click prop。

import { ComponentProps } from 'react';

const ButtonWithLogging = ({ onClick }: ComponentProps<"button">) => {
  const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => {
    console.log("Button clicked");
    onClick?.(e);
  };
  return <button {...props} onClick={handleClick} />;
};

此技巧也适用于自定义组件。

import { ComponentProps } from 'react';

// 自定义组件
const MyComponent = (props: { name: string }) => {
  // ...
};

const MyComponentWithLogging = (props: ComponentProps<typeof MyComponent>) => {
  // ...
};

84. 利用 MouseEventHandler、FocusEventHandler 等类型来实现简洁的类型

你无需手动输入事件处理程序,而是可以使用 MouseEventHandler 之类的类型来使代码更简洁、更易读。

import { MouseEventHandler,FocusEventHandler,ChangeEventHandler } from 'react';
// 这样写也没什么问题
const MyComponent = ({ onClick, onFocus, onChange }: {
  onClick: (e: MouseEvent<HTMLButtonElement>) => void;
  onFocus: (e: FocusEvent<HTMLButtonElement>) => void;
  onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}) => {
  // ...
};

// 更好的做法
const MyComponent = ({ onClick, onFocus, onChange }: {
  onClick: MouseEventHandler<HTMLButtonElement>;
  onFocus: FocusEventHandler<HTMLButtonElement>;
  onChange: ChangeEventHandler<HTMLInputElement>;
}) => {
  // ...
};

85. 当无法或不应从初始值推断类型时,请在 useState、useRef 等中明确指定类型

当无法从初始值推断出类型时,不要忘记指定类型。

例如,在下面的例子中,状态中存储了一个 selectedItemId,它应该是字符串或未定义。

由于未指定类型,TypeScript 会将类型推断为未定义,这不是我们想要的。

// 不好的做法: `selectedItemId`将会被推导为undefined
const [selectedItemId, setSelectedItemId] = useState(undefined);

// 推荐做法
const [selectedItemId, setSelectedItemId] = useState<string | undefined>(undefined);
注意:与此相反的是,当 TypeScript 可以为你推断类型时,你不需要指定类型。

86. 利用Record类型获得更清晰、更易于扩展的代码

假设我有一个代表日志级别的类型。

type LogLevel = "info" | "warn" | "error";

对于每个日志级别,我们都有一个相应的函数来记录消息。

const logFunctions = {
  info: (message: string) => console.info(message),
  warn: (message: string) => console.warn(message),
  error: (message: string) => console.error(message),
};

你可以使用 Record 类型,而不必手动输入 logFunctions的类型。

const logFunctions: Record<LogLevel, (message: string) => void> = {
  info: (message) => console.info(message),
  warn: (message) => console.warn(message),
  error: (message) => console.error(message),
};

使用 Record 类型可使代码更简洁、更易读,此外,如果添加或删除了新的日志级别,它还有助于捕获任何错误,例如,如果我决定添加调试日志级别,TypeScript 就会抛出错误。

87. 使用 as const 技巧来准确输入钩子返回值

假设我们有一个钩子 useIsHovered 来检测 div 元素是否处于悬停状态。

该钩子返回一个与 div 元素一起使用的 ref 和一个指示 div 是否处于悬停状态的布尔值。

const useIsHovered = () => {
  const ref = useRef<HTMLDivElement>(null);
  const [isHovered, setIsHovered] = useState(false);
  return [ref, isHovered]
};

目前,TypeScript 无法正确推断函数返回类型。

你可以通过明确输入返回类型来解决此问题,如下所示:

const useIsHovered = (): [RefObject<HTMLDivElement>, boolean] => {
  return [ref, isHovered]
};

或者你可以使用 as const 技巧来准确输入返回值:

const useIsHovered = () => {
  return [ref, isHovered] as const;
};

88. Redux:通过参考文档确保输入正确,以正确输入 Redux 状态和帮助程序。

如果你的项目是使用 Redux 来管理繁重的客户端状态,它也能很好地与 TypeScript 配合使用,你可以在此处找到有关如何将 Redux 与 TypeScript 结合使用的出色指南。

89. 使用 ComponentType 简化你的类型

假设你正在设计一款像 Figma 这样的应用,该应用由小组件组成,每个小组件都接受一个size prop。

为了重用逻辑,我们可以定义一个共享的 WidgetWrapper 组件,该组件采用 Widget 类型的小组件,定义如下:

interface Size {
  width: number;
  height: number
};

interface Widget {
  title: string;
  Component: ComponentType<{ size: Size }>;
}

WidgetWrapper 组件将呈现小组件并将相关尺寸传递给它。

const WidgetWrapper = ({ widget }: { widget: Widget }) => {
  const { Component, title } = widget;
  const { onClose, size, onResize } = useGetProps(); // 待做:更好的名字,但你应该能明白我的意思
  return (
    <Wrapper onClose={onClose} onResize={onResize}>
      <Title>{title}</Title>
      {/* 我们可以使用以下尺寸渲染组件 */}
      <Component size={size} />
    </Wrapper>
  );

90. 使用 TypeScript 泛型提高代码的可重用性

TypeScript 泛型使你的代码更具可重用性和灵活性。

例如,假设我在博客上有不同的项目(例如,帖子、关注者等),并且我想要一个通用列表组件来显示它们。

export interface Post {
  id: string;
  title: string;
  contents: string;
  publicationDate: Date;
}

export interface User {
  username: string;
}

export interface Follower extends User {
  followingDate: Date;
}

每个列表都应该可排序,有好的方法和不好的方法可以做到这一点。

不好的方法:创建了一个接受项目联合的列表组件。

这很糟糕,因为:

  • 每次添加新项目时,都必须更新函数/类型。
  • 该函数不是完全类型安全的(请参阅注释)。
  • 此代码依赖于其他文件(例如:FollowerItem、PostItem)。
  • 等等。
import { FollowerItem } from "./FollowerItem";
import { PostItem } from "./PostItem";
import { Follower, Post } from "./types";

type ListItem = { type: "follower"; follower: Follower } | { type: "post"; post: Post };

const ListBad = ({
  items,
  title,
  vertical = true,
  ascending = true,
}: {
  title: string;
  items: ListItem[];
  vertical?: boolean;
  ascending?: boolean;
}) => {
  const sortedItems = [...items].sort((a, b) => {
    const sign = ascending ? 1 : -1;
    return sign * compareItems(a, b);
  });

  return (
    <>
      <h3 className="title">{title}</h3>
      <div className={`list ${vertical ? "vertical" : ""}`}>
        {sortedItems.map((item) => (
          <div key={getItemKey(item)}>{renderItem(item)}</div>
        ))}
      </div>
    </>
  );
}

const compareItems = (a: ListItem, b: ListItem) => {
  if (a.type === "follower" && b.type === "follower") {
    return (
      a.follower.followingDate.getTime() - b.follower.followingDate.getTime()
    );
  } else if (a.type == "post" && b.type === "post") {
    return a.post.publicationDate.getTime() - b.post.publicationDate.getTime();
  } else {
    // This shouldn't happen
    return 0;
  }
}

const getItemKey = (item: ListItem) => {
  switch (item.type) {
    case "follower":
      return item.follower.username;
    case "post":
      return item.post.id;
  }
}

const renderItem = (item: ListItem) => {
  switch (item.type) {
    case "follower":
      return <FollowerItem follower={item.follower} />;
    case "post":
      return <PostItem post={item.post} />;
  }
}

相反,我们可以使用 TypeScript 泛型来创建更可重用且类型安全的列表组件。

前往这里查看一个完整的示例

91. 使用 NoInfer 类型确保输入值的推断准确

想象一下,你正在开发一款视频游戏,游戏有多个地点(例如,山谷、公路等),你想创建一个将玩家传送到新位置的函数。

const teleportPlayer = <L extends string>(
  position: Position,
  locations: L[],
  defaultLocation: L,
) : L => {
  // ...
}

该函数将按如下方式调用:

const position = { x: 1, y: 2, z: 3 };
teleportPlayer(position, ['LeynTir', 'Forin', 'Karin'], 'Forin');
teleportPlayer(position, ['LeynTir', 'Karin'], 'anythingCanGoHere'); // 这会起作用,但这是错误的,因为“anythingCanGoHere”不应该是一个有效的位置

第二个示例无效,因为 anythingCanGoHere 不是有效位置,但是,TypeScript 不会抛出错误,因为它从列表和默认位置推断出 L 的类型。

要解决此问题,请使用 NoInfer 实用程序类型。

const teleportPlayer = <L extends string>(
  position: Position,
  locations: L[],
  defaultLocation: NoInfer<L>,
) : NoInfer<L> => {
  // ...
}

现在 TypeScript 将抛出一个错误:

teleportPlayer(position, ['LeynTir', 'Karin'], 'anythingCanGoHere'); // 错误:类型为“anythingCanGoHere”的参数无法分配给类型为“LeynTir”|“Karin”的参数

使用 NoInfer 工具类型可确保默认位置必须是列表中提供的有效位置之一,从而防止无效输入。

说明: NoInfer类型自ts5.4开始提供。ts5.4版本以下可以使用如下的代码模拟实现:
type NoInfer<T> = [T][T extends any ? 0 : never];

92. 使用 ElementRef 类型定义ref的类型

有2种方法来定义 ref 的类型。

比较困难的方法是记住元素的类型名称并直接使用它。

const ref = useRef<HTMLDivElement>(null);

最简单的方法是使用 ElementRef 类型。这种方法更直接,因为你应该已经知道元素的名称。

import { ElementRef } from 'react';

const ref = useRef<ElementRef<"div">>(null);

十三. 其它技巧

93. 使用 eslint-plugin-react 和 Prettier 提高代码的质量和安全性。

如果你不使用 eslint-plugin-react,你就不能写出好的 React代码。它可以帮助你捕获潜在的错误并实施最佳实践。因此,请确保为你的项目安装和配置它。

你也可以使用 Prettier 自动格式化你的代码并确保你的代码库一致。

94. 使用 Sentry 或 Grafana Cloud Frontend Observability 等工具记录和监控你的应用程序。

你无法改进你没有测试的应用。如果你正在寻找用于生产应用程序的监控工具,请查看 SentryGrafana Cloud Frontend Observability

95. 使用在线 IDE 快速开始编码

设置本地开发环境可能很麻烦,尤其是对于初学者,因此,请从 Code SandboxStackblitz豆包jsbin码上掘金等在线 IDE 开始,这些工具可让你快速开始编码,而无需担心设置环境。

96. 想要学习高级 React 技能?看看这些书

如果你正在寻找高级 React 书籍 📚,我推荐:

97. 准备 React 面试?查看 reactjs-interview-questions

React 面试可能会比较棘手,幸运的是,你可以通过查看这个 repo 来做好准备。

98. 向 Nadia、Dan、Josh、Kent 等专家学习 React 最佳实践。

如果你想了解最佳实践并学习技巧,请务必关注以下专家:

99. 订阅本周 React 或 ui.dev 等新闻通讯,了解 React 生态系统的最新动态

React 是一个快速发展的生态系统。

有许多工具、库和最佳实践需要跟上。

要保持最新状态,请务必订阅新闻通讯,例如:

100. 在 r/reactjs 等平台上与 React 社区互动

React 社区非常棒。

你可以从其他开发人员那里学到很多东西并分享你的知识。

因此,请在 r/reactjs 等平台上与社区互动。

特别说明: 本次3篇上中下的文章参考了这篇文章,在原文的基础上有做相关改动。

夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。