IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    一些react使用小技巧(下)

    夕水发表于 2024-10-10 11:42:18
    love 0

    八. 测试 react 代码

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

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

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

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

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

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

    以下是两种使用方法:

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

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

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

    需要进行端到端测试吗?

    请务必查看 Cypress 或 Playwright。

    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 钩子是不必要的,因为它不需要是一个钩子。它不使用其他钩子,如 useEffect、useState 等。

    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 在你的应用中实现一流的数据获取

    数据获取可能非常棘手。

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

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

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

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

    • formik
    • React Hook Form
    • TanStack Form

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

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

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

    • Format.js
    • Lingui
    • react-i18next

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

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

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

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

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

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

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

    Shadcdn、Headless UI、acro design、ant 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 Glean 或 VSCode 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开始提供。

    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 等工具记录和监控你的应用程序。

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

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

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

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

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

    • adevnadia 的《Advanced React》
    • TejasKumar_ 的《Fluent React》
    • addyosmani 和 djirdehh 的《Building Large Scale Web Apps》

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

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

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

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

    • @adevnadia:https://x.com/adevnadia 了解高级 React 技巧
    • @joshwcomeau:https://x.com/joshwcomeau
    • @kentcdodds:https://x.com/kentcdodds
    • @mattpocockuk:https://x.com/mattpocockuk 了解 TypeScript 技巧
    • @tejaskumar_:https://x.com/TejasKumar_
    • @housecor:https://x.com/housecor
    • @_ndeyefatoudiop:https://x.com/_ndeyefatoudiop

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

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

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

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

    • @sebastienlorber 的This Week In React
    • ui.dev
    • 等等。

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

    React 社区非常棒。

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

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

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


沪ICP备19023445号-2号
友情链接