- 译:101个React技巧#1组件组织
- 译:101个React技巧#2有效的设计模式与技术
- 译:101个React技巧#3Keys&Refs
- 译:101个React技巧#4组织React代码
- 译:101个React技巧#5高效状态管理
- 译:101个React技巧#6React代码优化
- 译:101个React技巧#7React代码调试技巧
- 译:101个React技巧#8测试 React代码
- 译:101个React技巧#9React hook
- 译:101个React技巧#10必知的React库/工具
- 译:101个React技巧#11React与Visual Studio Cod
- 译:101个React技巧#12React 与 TypeScript
- 译:101个React技巧#13其他技巧
40. 使用 memo 防止不必要的重新渲染
当处理渲染成本高的组件且其父组件频繁更新时,使用 memo 可以带来显著改变。
memo 确保组件仅在 props 发生变化时重新渲染,而不是仅仅因为父组件重新渲染。
在下面的示例中,我通过 useGetDashboardData 从服务器获取数据。如果 posts 没有变化,用 memo 包裹 ExpensiveList 可以防止其他数据更新时重新渲染。
export function App() {
const { profileInfo, posts } = useGetDashboardData();
return (
<div className="App">
<h1>Dashboard</h1>
<Profile data={profileInfo} />
<ExpensiveList posts={posts} />
</div>
);
}
const ExpensiveList = memo(({ posts }) => {
/// 其余实现
});
💡: 一旦 React 编译器 稳定后,这个技巧可能就无关紧要了 😅。
41. 使用 memo 指定相等性函数来指导 React 如何比较 props
默认情况下,memo 使用 Object.is 来比较每个 prop 与其之前的值。
然而,对于更复杂或特定的场景,指定自定义相等性函数可能比默认比较或重新渲染更高效。
示例 👇
const ExpensiveList = memo(
({ posts }) => {
return <div>{JSON.stringify(posts)}</div>;
},
(prevProps, nextProps) => {
// 仅当最后一篇文章或列表大小变化时重新渲染
const prevLastPost = prevProps.posts[prevProps.posts.length - 1];
const nextLastPost = nextProps.posts[nextProps.posts.length - 1];
return (
prevLastPost.id === nextLastPost.id &&
prevProps.posts.length === nextProps.posts.length
);
}
);
42. 声明 memoized 组件时优先使用命名函数而非箭头函数
定义 memoized 组件时,使用命名函数而非箭头函数可以提高 React DevTools 中的清晰度。
箭头函数通常会导致通用名称如 _c2,使调试和分析更加困难。
❌ 不好: 对 memoized 组件使用箭头函数会导致 React DevTools 中显示的信息较少。
const ExpensiveList = memo(({ posts }) => {
/// 其余实现
});
ExpensiveList 名称不可见
✅ 好: 组件名称将在 DevTools 中可见。
const ExpensiveList = memo(function ExpensiveListFn({ posts }) {
/// 其余实现
});
你可以看到 DevTools 中的 ExpensiveListFn
43. 使用 useMemo 缓存昂贵计算或保留引用
我通常会在以下情况使用 useMemo:
- 当我有不应该在每次渲染时重复执行的昂贵计算时
- 如果计算值是一个 非原始值,并且被用作
useEffect等钩子的依赖项 - 计算出的 非原始 值将作为 prop 传递给用
memo包裹的组件;否则,这会破坏记忆化,因为 React 使用 Object.is 来检测 props 是否变化
❌ 不好: ExpensiveList 的 memo 不能防止重新渲染,因为样式在每次渲染时都被重新创建。
export function App() {
const { profileInfo, posts, baseStyles } = useGetDashboardData();
// 每次渲染都会得到一个新的 `styles` 对象
const styles = { ...baseStyles, padding: "10px" };
return (
<div className="App">
<h1>Dashboard</h1>
<Profile data={profileInfo} />
<ExpensiveList posts={posts} styles={styles} />
</div>
);
}
const ExpensiveList = memo(function ExpensiveListFn({ posts, styles }) {
/// 其余实现
});
✅ 好: 使用 useMemo 确保 styles 仅在 baseStyles 变化时变化,让 memo 有效防止不必要的重新渲染。
export function App() {
const { profileInfo, posts, baseStyles } = useGetDashboardData();
// 仅在 `baseStyles` 变化时得到新的 `styles` 对象
const styles = useMemo(
() => ({ ...baseStyles, padding: "10px" }),
[baseStyles]
);
return (
<div className="App">
<h1>Dashboard</h1>
<Profile data={profileInfo} />
<ExpensiveList posts={posts} styles={styles} />
</div>
);
}
44. 使用 useCallback 记忆化函数
useCallback 与 useMemo 类似,但专门用于记忆化函数。
❌ 不好: 每当主题变化时,handleThemeChange 会被调用两次,我们会向服务器推送两次日志。
function useTheme() {
const [theme, setTheme] = useState("light");
// `handleThemeChange` 在每次渲染时都会变化
// 因此,每次渲染后都会触发 effect
const handleThemeChange = (newTheme) => {
pushLog(["Theme changed"], {
context: {
theme: newTheme,
},
});
setTheme(newTheme);
};
useEffect(() => {
const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
handleThemeChange(dqMediaQuery.matches ? "dark" : "light");
const listener = (event) => {
handleThemeChange(event.matches ? "dark" : "light");
};
dqMediaQuery.addEventListener("change", listener);
return () => {
dqMediaQuery.removeEventListener("change", listener);
};
}, [handleThemeChange]);
return theme;
}
✅ 好: 用 useCallback 包裹 handleThemeChange 确保它仅在必要时重新创建,减少不必要的执行。
const handleThemeChange = useCallback((newTheme) => {
pushLog(["Theme changed"], {
context: {
theme: newTheme,
},
});
setTheme(newTheme);
}, []);
45. 记忆化工具钩子返回的回调或值以避免性能问题
当你创建一个自定义钩子与他人共享时,记忆化返回的值和函数至关重要。
这种做法使你的钩子更高效,并防止使用它的人遇到不必要的性能问题。
❌ 不好: loadData 未被记忆化,导致性能问题。
function useLoadData(fetchData) {
const [result, setResult] = useState({
type: "notStarted",
});
async function loadData() {
setResult({ type: "loading" });
try {
const data = await fetchData();
setResult({ type: "loaded", data });
} catch (err) {
setResult({ type: "error", error: err });
}
}
return { result, loadData };
}
✅ 好: 我们记忆化所有内容,因此没有意外的性能问题。
function useLoadData(fetchData) {
const [result, setResult] = useState({
type: "notStarted",
});
// 用 `useRef` 包裹并使用 `ref` 值,使函数永不变化
const fetchDataRef = useRef(fetchData);
useEffect(() => {
fetchDataRef.current = fetchData;
}, [fetchData]);
// 用 `useCallback` 包裹并使用 `ref` 值,使函数永不变化
const loadData = useCallback(async () => {
setResult({ type: "loading" });
try {
const data = await fetchDataRef.current();
setResult({ type: "loaded", data });
} catch (err) {
setResult({ type: "error", error: err });
}
}, []);
return useMemo(() => ({ result, loadData }), [result, loadData]);
}
46. 利用懒加载和 Suspense 让你的应用加载更快
构建应用时,考虑对以下代码使用懒加载和 Suspense:
- 加载成本高的代码
- 仅与部分用户相关的功能(如高级功能)
- 初始用户交互不需要的代码
在下面的 沙盒 中 👇,Slider 资源(JS + CSS)仅在点击卡片后加载。
47. 节流你的网络以模拟慢速网络
你知道可以直接在 Chrome 中模拟慢速互联网连接吗?
这在以下情况下特别有用:
- 客户报告你无法在更快的网络上复现的慢加载时间
- 你正在实现懒加载,并希望观察文件在较慢条件下的加载情况,以确保适当的加载状态
48. 使用 react-window 或 react-virtuoso 高效渲染列表
切勿一次性渲染长列表项——如聊天消息、日志或无限列表。
这样做可能导致浏览器冻结。
相反,虚拟化列表。这意味着仅渲染用户可能看到的项目子集。
像 react-window、react-virtuoso 或 @tanstack/react-virtual 这样的库就是为此设计的。
❌ 不好: NonVirtualList 一次性渲染所有 50,000 条日志行,即使它们不可见。
function NonVirtualList({ items }) {
return (
<div style={{ height: "100%" }}>
{items.map((log, index) => (
<div
key={log.id}
style={{
padding: "5px",
borderBottom:
index === items.length - 1 ? "none" : "1px solid #ccc",
}}
>
<LogLine log={log} index={index} />
</div>
))}
</div>
);
}
✅ 好: VirtualList 仅渲染可能可见的项目。
function VirtualList({ items }) {
return (
<Virtuoso
style={{ height: "100%" }}
data={items}
itemContent={(index, log) => (
<div
key={log.id}
style={{
padding: "5px",
borderBottom:
index === items.length - 1 ? "none" : "1px solid #ccc",
}}
>
<LogLine log={log} index={index} />
</div>
)}
/>
);
}
你可以在下面的沙盒中切换这两个选项,并注意使用 NonVirtualList 时应用的性能有多差 👇。