useEffect 最佳实践
移除不必要的 useEffect
- 对于渲染所需的数据,如果可以用组件内状态(
props、state)转换而来,转换操作避免放在Effect中,而应该直接放在 FC 函数体中。
如果转换计算的消耗比较大,可以用useMemo进行缓存。 - 对于一些用户行为引起数据变化,其后续的逻辑不应该放在
Effect中,而是在事件处理函数中执行逻辑即可
当 props 变化时重置所有 state
比如,一个组件需要在用户切换的时候,重置所有的 state ,我们平常会这么做:
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState("");
// 🔴 避免:当 prop 变化时,在 Effect 中重置 state
useEffect(() => {
setComment("");
}, [userId]);
// ...
}我们可以利用组件**key不同将会完全重新渲染**的特点解决这个问题,只需要在父组件中给这个组件传递一个与props同步的key值即可:
export default function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}订阅外部 store
我们曾经常用的做法是在Effect中编写事件监听的逻辑:
function useOnlineStatus() {
// 不理想:在 Effect 中手动订阅 store
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}
updateState();
window.addEventListener("online", updateState);
window.addEventListener("offline", updateState);
return () => {
window.removeEventListener("online", updateState);
window.removeEventListener("offline", updateState);
};
}, []);
return isOnline;
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}这里可以换成useSyncExternalStore这个 hook
function subscribe(callback) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
function useOnlineStatus() {
// ✅ 非常好:用内置的 Hook 订阅外部 store
return useSyncExternalStore(
subscribe, // 只要传递的是同一个函数,React 不会重新订阅
() => navigator.onLine, // 如何在客户端获取值
() => true // 如何在服务端获取值
);
}
function ChatIndicator() {
const isOnline = useOnlineStatus(); // …
}在 useEffect 中请求数据
竞态问题
在 useEffect 中请求数据要面临的第一个问题是「需要解决竞态问题」**。
这里有个开发阶段很难复现的bug —— 如果userID变化足够快,会发起多个不同的用户请求。
而最终展示哪个用户的数据,取决于哪个请求先返回。这就是「请求的竞态问题」**。
可以通过增加一个 ignore 标识,来保证只有最后一次的回调是有效的
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
// 说白了用一个ignore变量来控制这个Effect回调的"有效性",
let ignore = false;
fetchResults(query, page).then((json) => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
} // …
}点击返回按钮后重新请求数据
如果用户跳转到新的页面后,又通过浏览器回退按钮回到当前页面,并不能立刻看到他跳转前的页面。
相反,看到的可能是个白屏 —— 因为还需要重新执行useEffect获取初始数据。
这个问题的本质原因是:没有初始数据的缓存。
CSR 时的白屏时间
CSR(Client-Side Rendering,客户端渲染)时在 useEffect中请求数据,在数据返回前页面都是白屏状态。
瀑布问题
如果父子组件都依赖useEffect获取初始数据渲染,那么整个渲染流程如下:
- 父组件
mount - 父组件
useEffect执行,请求数据 - 数据返回后重新渲染父组件
- 子组件
mount - 子组件
useEffect执行,请求数据 - 数据返回后重新渲染子组件
可见,当父组件数据请求成功后子组件甚至还没开始首屏渲染。
这就是渲染中的瀑布问题 —— 数据像瀑布一样一级一级向下流动,流到的组件才开始渲染,很低效。
解决方式
- 对于 SSR,可以使用 NextJs、Remix 接管数据请求。
- 对于 CSR,可以使用 React Query、useSWR 接管数据请求
- 如果不想使用这些方案,想自己写,可以参考
React新文档中下面两篇文章: