八. 测试 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 等库简化表单状态管理
如果你在使用表单时遇到困难,推荐可以看看这些库。
71. 使用 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开始提供。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 等工具记录和监控你的应用程序。
你无法改进你没有测试的应用。如果你正在寻找用于生产应用程序的监控工具,请查看 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 是一个快速发展的生态系统。
有许多工具、库和最佳实践需要跟上。
要保持最新状态,请务必订阅新闻通讯,例如:
100. 在 r/reactjs 等平台上与 React 社区互动
React 社区非常棒。
你可以从其他开发人员那里学到很多东西并分享你的知识。
因此,请在 r/reactjs 等平台上与社区互动。
特别说明: 本次3篇上中下的文章参考了这篇文章,在原文的基础上有做相关改动。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。