原文: React.useEffect Hook – Common Problems and How to Fix Them
大多数开发人员都非常熟悉 React hooks 的工作方式和常见用例,但是有一个 useEffect
问题很多人可能不太清楚。
用例
让我们从一个简单的场景开始。我们正在构建一个 React 应用程序,希望在一个组件中显示当前用户的用户名。但首先,我们需要从 API 中获取用户名。
因为我们知道我们也需要在应用程序的其他地方使用用户数据,所以我们还想在自定义 React hook 中抽象数据获取逻辑。
我们希望 React 组件看起来像这样:
const Component = () => {
// useUser custom hook
return <div>{user.name}</div>;
};
看起来很简单!
useUser React hook
第二步是创建我们的 useUser
自定义钩子。
const useUser = (user) => {
const [userData, setUserData] = useState();
useEffect(() => {
if (user) {
fetch("users.json").then((response) =>
response.json().then((users) => {
return setUserData(users.find((item) => item.id === user.id));
})
);
}
}, []);
return userData;
};
让我们分解一下。我们正在检查钩子是否正在接收用户对象。之后,我们从名为 users.json
的文件中获取用户列表,并对其进行过滤以找到具有我们需要的 id 的用户。
然后,一旦我们获得了必要的数据,我们就将其保存为钩子的 userData
状态。最后返回 userData
。
注意:这是一个仅用于说明目的的示例!现实世界中的数据获取要复杂得多。如果你对该主题感兴趣,请查看我关于如何使用 ReactQuery、Typescript 和 GraphQL 创建出色的数据获取设置的文章。
让我们在 React 组件中插入钩子,看看会发生什么。
const Component = () => {
const user = useUser({ id: 1 });
return <div>{user?.name}</div>;
};
好的,一切都按预期进行。但是等等……这是什么?
ESLint exhaustive-deps 规则
我们的钩子中有一个 ESLint 警告:
React Hook useEffect has a missing dependency: 'user'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
嗯,我们的 useEffect
似乎缺少依赖项。那好吧! 让我们添加它。可能发生的最坏情况是什么? 😂
const useUser = (user) => {
const [userData, setUserData] = useState();
useEffect(() => {
if (user) {
fetch("users.json").then((response) =>
response.json().then((users) => {
return setUserData(users.find((item) => item.id === user.id));
})
);
}
}, [user]);
return userData;
};
看起来我们的组件 Component
现在不会停止重新渲染。这里发生了什么?!
让我们解释一下。
无限重新渲染问题
我们的组件重新渲染的原因是因为 useEffect
依赖项在不断变化。但为什么?我们总是将相同的对象传递给钩子!
虽然我们确实传递了一个具有相同键和值的对象,但它并不是完全相同的对象。每次重新渲染组件时,我们实际上都是在创建一个新对象,然后我们将新对象作为参数传递给 useUser
钩子。
在内部,useEffect
比较两个对象,由于它们有不同的引用,它再次获取用户并将新的用户对象设置为状态。状态更新然后触发组件中的重新渲染,不断重复……
所以,我们能做些什么?
如何修复
现在我们了解了问题,可以开始寻找解决方案。
第一个也是最明显的选择是从 useEffect
依赖数组中移除依赖,忽略 ESLint 规则,继续我们的生活。
但这是错误的做法。它可以(并且可能会)导致我们的应用程序中出现错误和意外行为。如果你想了解更多有关 useEffect
如何工作的信息,我强烈推荐 Dan Abramov 的完整指南。
下一个是什么?
在我们的例子中,最简单的解决方案是从组件中取出 { id: 1 }
对象。这将为对象提供稳定的引用并解决我们的问题。
const userObject = { id: 1 };
const Component = () => {
const user = useUser(userObject);
return <div>{user?.name}</div>;
};
export default Component;
但这并不总是可能的。想象一下,用户 id 以某种方式依赖于组件 props 或状态。
例如,可能是我们使用 URL 参数来访问它。如果是这种情况,我们可以使用一个方便的 useMemo
钩子来记忆对象并再次确保稳定的引用。
const Component = () => {
const { userId } = useParams();
const userObject = useMemo(() => {
return { id: userId };
}, [userId]); // Don't forget the dependencies here either!
const user = useUser(userObject);
return <div>{user?.name}</div>;
};
export default Component;
最后,不是将对象变量传递给我们的 useUser
钩子,而是可以只传递用户 ID 本身,这是一个原始值。这将防止 useEffect
钩子中的引用相等问题。
const useUser = (userId) => {
const [userData, setUserData] = useState();
useEffect(() => {
fetch("users.json").then((response) =>
response.json().then((users) => {
return setUserData(users.find((item) => item.id === userId));
})
);
}, [userId]);
return userData;
};
const Component = () => {
const user = useUser(1);
return <div>{user?.name}</div>;
};
问题解决啦!
在此过程中,我们甚至不必违反任何 ESLint 规则......
注意:如果我们传递给自定义钩子的参数是一个函数,而不是一个对象,我们将使用非常相似的技术来避免无限重新渲染。一个显着的区别是我们必须在上面的例子中用 useCallback
替换 useMemo
。
感谢你阅读本文!
对代码感到好奇吗?在这里探索吧。
欢迎访问我的博客并在 Twitter 上关注我,以获取更多与 React 相关的内容。
图片作者:vectorjuice