React钩子已经存在了一段时间了。大多数开发人员已经非常熟悉它们的工作方式和常见的使用情况。但有一个useEffect ,我们中的很多人总是上当受骗。
用例
让我们从一个简单的场景开始。我们正在构建一个React应用,我们想在我们的一个组件中显示当前用户的用户名。但首先,我们需要从一个API中获取用户名。
因为我们知道我们还需要在我们应用的其他地方使用用户数据,所以我们还想在一个自定义的React钩子中抽象出数据获取逻辑。
基本上,我们希望我们的React组件看起来像这样:
const Component = () => {
// useUser custom hook
return <div>{user.name}</div>;
};
看起来很简单
useUser React钩子
第二步将是创建我们的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穷举规则
我们的钩子里有一个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 依赖关系在不断变化。但是为什么呢?我们总是在向我们的钩子传递相同的对象!为什么?
虽然我们确实在传递一个具有相同键和值的对象,但这并不是完全相同的对象。实际上,每次我们重新渲染我们的Component ,我们都会创建一个新的对象。然后我们把新的对象作为一个参数传递给我们的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在某种程度上是依赖于组件的道具或状态的。
例如,可能是我们正在使用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 。
谢谢你的阅读!