Appearance
使用useEffect的正确姿势
为什么useEffect必不可少,以及为什么在数据获取时应避免使用它。
最近我开始学习前端开发的基础知识。我观察到,许多前端开发者通常会使用useEffect来获取数据。然而,在大多数情况下,这并不是一种理想的做法。在本文中,我将解释为什么不推荐这样做,以及我们如何替代或者改进这种方法。但首先,我们需要理解什么是副作用以及useEffect是如何工作的。
理解React中的副作用
在 React 中,副作用是指发生在组件外部的后果性行为。理想情况下,React 组件应该是完全纯粹的,这意味着它们不应修改调用之前存在的任何对象或变量,并且对相同输入应始终产生相同输出。例如:
jsx
function Square({ number }) {
return (
<div>
The square of {number} is {number * number}.
</div>
);
}
function Square({ number }) {
return (
<div>
The square of {number} is {number * number}.
</div>
);
}
上面的例子看起来很简单,但它遵守了上述的基本规则,保证了代码的可预测性。
然而,有时我们必须在代码中引入副作用,例如触发动画或从外部 API 获取数据。在这种情况下,我们可以使用 React 的事件处理函数(例如点击按钮时触发的函数)。但如果我们需要在组件挂载后立即触发副作用,可以使用 useEffect 钩子。
useEffect 的工作原理
useEffect 钩子会在组件初次渲染后立即在客户端运行。它接受两个参数:一个包含副作用逻辑的函数,以及一个依赖项数组。当组件重新渲染时,如果依赖项数组中的某些值发生了变化,useEffect 中的函数会再次触发。
jsx
useEffect(() => {
// 副作用函数
console.log(randomValue);
}, [randomValue]); // 依赖项数组
useEffect(() => {
// 副作用函数
console.log(randomValue);
}, [randomValue]); // 依赖项数组
在useEffect中,还可以返回一个清理函数,该函数会在组建卸载前调用。清理函数用于还原副作用的更改,例如关闭连接,移除事件监听器或者清除计时器。它还可以帮助避免在获取数据时发生竞态条件(race condition)。
jsx
useEffect(() => {
const options = { roomId };
const connection = createConnection();
connection.connect();
return () => connection.disconnect(); // 清理函数
}, [roomId]);
useEffect(() => {
const options = { roomId };
const connection = createConnection();
connection.connect();
return () => connection.disconnect(); // 清理函数
}, [roomId]);
为什么需要useEffect
React 组件通过 useEffect 与外部系统进行同步非常重要,因为 React 有其特定的生命周期阶段:挂载、渲染和卸载。而外部 API 通常不会与这些生命周期阶段完全匹配。因此,useEffect 对于这种同步至关重要,因为 React 本身无法直接感知外部操作,例如数据获取或分析。
一个典型的例子是,在用户按下 Escape 键时关闭模态框。在这种场景中,useEffect 可帮助我们实现与用户行为的同步:
jsx
const Modal = ({ closeModal }) => {
React.useEffect(() => {
function handleKeyDown(event) {
if (event.code === "Escape") {
closeModal();
}
}
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
return <div>.....</div>;
};
const Modal = ({ closeModal }) => {
React.useEffect(() => {
function handleKeyDown(event) {
if (event.code === "Escape") {
closeModal();
}
}
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
return <div>.....</div>;
};
通过 useEffect,我们可以确保事件监听器正确管理,并与 Modal 组件的生命周期保持同步,从而高效地处理用户按键事件。
为什么我们不应该用 useEffect 获取数据
听起来有点矛盾,对吧?如果 useEffect 是用来同步外部数据的,为什么不推荐用它来获取数据呢?答案很简单,因为我们有更好的选择。
在实际项目中,仅仅通过 useEffect 获取数据往往不够。这样做可能导致以下问题:
• 竞态条件(Race Conditions)
• 不必要的带宽消耗
• 错误可能导致应用崩溃
为了改进,我们可以引入缓存来减少带宽消耗,处理错误、实现失败重试,以及通过去重避免竞态条件。但这些改进会带来扩展性、维护性和调试上的挑战。
我们有哪些替代方案?
如果你使用的是基于 React 的框架(例如 Next.js),它本身就有内置的数据获取机制。在较新的版本中,甚至可以直接在服务端获取数据。
同时,还有一些优秀的第三方库,比如 SWR 和 React Query。这些库可以简化数据获取的流程,并减少需要编写的代码量。
如果没有使用框架,而是直接使用 React 来获取数据,官方建议通过忽略旧请求来处理竞态条件。此外,还可以将 useEffect 的逻辑提取到自定义 Hook 中,以提高代码的可维护性。
以下是一个例子:
jsx
useEffect(() => {
let ignore = false;
fetch(url)
.then((response) => response.json())
.then((json) => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}, [url]);
useEffect(() => {
let ignore = false;
fetch(url)
.then((response) => response.json())
.then((json) => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}, [url]);
通过这种“忽略”的技巧,可以避免组件在快速切换时因数据响应延迟而显示错误内容。例如,用户点击第1页后又快速切换到第2页时,第1页的数据响应到达后不会覆盖第2页的数据。
结论
由于扩展性问题,仅通过 useEffect 获取数据并不是最佳选择。建议使用外部库或基于 React 的框架(如 Next.js)。
希望本文能帮助你更好地理解 useEffect,并引导你朝着更加可维护、易调试的代码方向努力。