- 译: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其他技巧
59. 确保在 useEffect 钩子中执行必要的清理
如果你在 useEffect 钩子中设置了任何需要后续清理的内容,请始终返回一个清理函数。
这可以是任何内容,从结束聊天会话到关闭数据库连接。
忽略这一步可能导致资源使用不当和潜在的内存泄漏。
❌ 不好: 这个例子设置了一个间隔计时器,但我们从未清除它,这意味着即使在组件卸载后它也会继续运行。
function Timer() {
const [time, setTime] = useState(new Date());
useEffect(() => {
setInterval(() => {
setTime(new Date());
}, 1_000);
}, []);
return <>当前时间 {time.toLocaleTimeString()}</>;
}
✅ 好: 当组件卸载时,间隔计时器被正确清除。
function Timer() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setTime(new Date());
}, 1_000);
// 我们清除间隔计时器
return () => clearInterval(intervalId);
}, []);
return <>当前时间 {time.toLocaleTimeString()}</>;
}
60. 使用 refs 访问 DOM 元素
你永远不应该直接用 React 操作 DOM。
像 document.getElementById 和 document.getElementsByClassName 这样的方法是被禁止的,因为 React 应该访问/操作 DOM。
那么当你需要访问 DOM 元素时应该怎么做呢?
你可以使用 useRef 钩子,就像下面的例子中我们需要访问 canvas 元素一样。
注意: 我们可以给 canvas 添加一个 ID 并使用
document.getElementById,但这不被推荐。
61. 使用 refs 在重新渲染间保留值
如果你的 React 组件中有不在状态中的可变值,你会发现这些值的更改不会在重新渲染间保留。
除非你将它们保存在全局中,否则就会发生这种情况。
你可能会考虑将这些值放入状态中。然而,如果它们与渲染无关,这样做会导致不必要的重新渲染,浪费性能。
这就是 useRef 的用武之地。
在下面的例子中,我想在用户点击某个按钮时停止计时器。为此,我需要将 intervalId 存储在某个地方。
❌ 不好: 下面的例子不会按预期工作,因为 intervalId 在每次组件重新渲染时都会被重置。
function Timer() {
const [time, setTime] = useState(new Date());
let intervalId;
useEffect(() => {
intervalId = setInterval(() => {
setTime(new Date());
}, 1_000);
return () => clearInterval(intervalId);
}, []);
const stopTimer = () => {
intervalId && clearInterval(intervalId);
};
return (
<>
<>当前时间: {time.toLocaleTimeString()} </>
<button onClick={stopTimer}>停止计时器</button>
</>
);
}
✅ 好: 通过使用 useRef,我们确保 interval ID 在重新渲染间被保留。
function Timer() {
const [time, setTime] = useState(new Date());
const intervalIdRef = useRef();
const intervalId = intervalIdRef.current;
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date());
}, 1_000);
intervalIdRef.current = interval;
return () => clearInterval(interval);
}, []);
const stopTimer = () => {
intervalId && clearInterval(intervalId);
};
return (
<>
<>当前时间: {time.toLocaleTimeString()} </>
<button onClick={stopTimer}>停止计时器</button>
</>
);
}
62. 在钩子如 useEffect 中优先使用命名函数而非箭头函数,以便在 React Dev Tools 中轻松找到它们
如果你有很多钩子,在 React DevTools 中找到特定的钩子可能会有挑战性。
一个技巧是使用命名函数,这样你可以快速定位它们。
❌ 不好: 在很多钩子中找到特定的 effect 很困难。
function HelloWorld() {
useEffect(() => {
console.log("🚀 ~ Hello, I just got mounted");
}, []);
return <>Hello World</>;
}
Effect 没有关联的名称
✅ 好: 你可以快速定位 effect。
function HelloWorld() {
useEffect(function logOnMount() {
console.log("🚀 ~ Hello, I just got mounted");
}, []);
return <>Hello World</>;
}
Effect 有关联的名称
63. 使用自定义钩子封装逻辑
假设我有一个组件,它从用户的深色模式偏好中获取主题并在应用中使用它。
最好将返回主题的逻辑提取到一个自定义钩子中(以便重用并保持组件简洁)。
❌ 不好: App 过于拥挤
function 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 简洁多了,我们可以重用逻辑
function App() {
const theme = useTheme();
return (
<div className={`App ${theme === "dark" ? "dark" : ""}`}>Hello Word</div>
);
}
// 可以重用的自定义钩子
function 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;
}
64. 优先使用函数而非自定义钩子
永远不要把逻辑放在钩子里,当可以使用函数时 🛑。
实际上:
- 钩子只能在其他钩子或组件中使用,而函数可以在任何地方使用。
- 函数比钩子更简单。
- 函数更容易测试。
- 等等。
❌ 不好: useLocale 钩子是不必要的,因为它不需要是一个钩子。它没有使用其他钩子如 useEffect、useState 等。
function App() {
const locale = useLocale();
return (
<div className="App">
<IntlProvider locale={locale}>
<BlogPost post={EXAMPLE_POST} />
</IntlProvider>
</div>
);
}
function useLocale() {
return window.navigator.languages?.[0] ?? window.navigator.language;
}
✅ 好: 创建一个函数 getLocale 代替
function App() {
const locale = getLocale();
return (
<div className="App">
<IntlProvider locale={locale}>
<BlogPost post={EXAMPLE_POST} />
</IntlProvider>
</div>
);
}
function getLocale() {
return window.navigator.languages?.[0] ?? window.navigator.language;
}
65. 使用 useLayoutEffect 钩子防止视觉 UI 闪烁
当一个 effect 不是由用户交互引起时,用户会在 effect 运行之前看到 UI(通常很短暂)。
因此,如果 effect 修改了 UI,用户会先看到初始 UI 版本,然后很快看到更新后的版本,造成视觉闪烁。
使用 useLayoutEffect 确保 effect 在所有 DOM 变更后同步运行,防止初始渲染时的闪烁。
在下面的沙盒中,我们希望宽度在列之间均匀分布(我知道这可以用 CSS 完成,但我需要一个例子 😅)。
使用 useEffect,你可以在开始时短暂地看到表格在变化。列首先以默认大小渲染,然后才调整到正确的大小。
如果你在寻找另一个很好的用法,看看这篇文章。
66. 使用 useId 钩子为可访问性属性生成唯一 ID
厌倦了想 ID 或者它们冲突?
你可以使用 useId 钩子在 React 组件内部生成一个唯一 ID,并确保你的应用是可访问的。
例子
function Form() {
const id = useId();
return (
<div className="App">
<div>
<label>
姓名 <input type="text" aria-describedby={id} />
</label>
</div>
<span id={id}>确保包含全名</span>
</div>
);
}
67. 使用 useSyncExternalStore 订阅外部存储
这是一个很少需要但超级强大的钩子 💪。
在以下情况下使用这个钩子:
- 你有一些状态不在 React 树中可访问(即不在状态或上下文中)
- 状态可以改变,你需要你的组件被通知这些变化
在下面的例子中,我想要一个 Logger 单例来记录我的整个应用中的错误、警告、信息等。
这些是需求:
- 我需要能够在 React 应用中的任何地方调用它(甚至在非 React 组件中),所以我不会把它放在状态/上下文中。
- 我想在一个
Logs组件中向用户显示所有日志
👉 我可以在 Logs 组件中使用 useSyncExternalStore 来访问日志并监听变化。
function createLogger() {
let logs = [];
let listeners = [];
const pushLog = (log) => {
logs = [...logs, log];
listeners.forEach((listener) => listener());
};
return {
getLogs: () => Object.freeze(logs),
subscribe: (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter((l) => l !== listener);
};
},
info: (message) => {
pushLog({ level: "info", message });
console.info(message);
},
error: (message) => {
pushLog({ level: "error", message });
console.error(message);
},
warn: (message) => {
pushLog({ level: "warn", message });
console.warn(message);
},
};
}
export const Logger = createLogger();
68. 使用 useDeferredValue 钩子在新的结果可用前显示之前的查询结果
想象你正在构建一个在地图上显示国家的应用。
用户可以筛选以查看特定最大人口规模的国家。
每次 maxPopulationSize 更新时,地图都会重新渲染(见下面的沙盒)。
因此,注意当你移动滑块太快时它是多么卡顿。这是因为每次滑块移动时地图都会重新渲染。
为了解决这个问题,我们可以使用 useDeferredValue 钩子,使滑块更新更流畅。
<Map
maxPopulationSize={deferredMaxPopulationSize}
// …
/>
如果你在寻找另一个很好的用法,看看这篇文章。