React.useEffect钩子--常见的问题和如何修复它们

134 阅读4分钟

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

谢谢你的阅读!