【问题标题】:React Hooks - useDidUpdateEffect with deep compareReact Hooks - useDidUpdateEffect 与深度比较
【发布时间】:2021-11-11 00:09:30
【问题描述】:

我有以下 useDidUpdateEffect 钩子

import { useRef, useEffect } from "react";

export default function useDidUpdateEffect(effect, deps) {
  const didMount = useRef(false);

  useEffect(() => {
    if (didMount.current) {
      effect();
    } else {
      didMount.current = true;
    }
  }, deps);
}

而我目前的使用方式如下:

useDidUpdateEffect(() => { ... }, [JSON.stringify(complexNestedObject)]);

问题在于,由于我的 dep 复杂而深入,有时会在没有必要的情况下执行回调。

我有一个useEffect的解决方案,就是useDeepEffect:

import { useRef, useEffect } from "react";
import { isEqual } from "lodash";

export default function useDeepEffect(effect, deps = undefined) {
  const isFirst = useRef(true);
  const prevDeps = useRef(deps);

  useEffect(() => {
    const isSame = prevDeps.current.every((obj, index) =>
      isEqual(obj, deps[index])
    );

    if (isFirst.current || !isSame) {
      effect();
    }

    isFirst.current = false;
    prevDeps.current = deps;
  }, deps);
}

但我想不出什么可以与 useDidUpdateEffect 进行深入比较,而不必创建另一个挂钩。

有什么想法吗?

=====================

更新:

由于这种情况,我处于当前情况。也许这可以做得更简单,但说实话,我已经尽力了(我不是超级专业)

  1. 我有一个包含不同模块的文件夹“api”,其中包含连接到我的服务器的不同功能(我没有使用挂钩 这部分)
   services/
       firebase/
           api/
              users/
                 helpers/
                 cache/
                    usersCache.js
                 index.js
                 ...

如您所见,我有一个模块“usersCache.js”(记忆模式),它将避免一些 RTT 并降低成本。为了避免 RAM 出现问题,我在那里实现了一个缓存算法“LRU”。

  1. 在我的应用程序中我没有使用 REDUX(可能是我采取的最糟糕的想法,但为时已晚,90% 的工作已经完成。将尝试学习 这项技术并在生产中采用它,并进行长时间的重构)。

为了管理复杂的状态,我使用了 React Context + useReducer,这在某种程度上简化了我的生活,因为它有点类似于 Redux。

就我而言,对于用户,我有 2 个上下文:

  1. 当前用户上下文
  2. 其他用户上下文

它们都包含获取的用户的敏感和非敏感数据。这里你可能会想:为什么要有usersCache?如果两个上下文都可以用作内存缓存?

答案(至少我认为)是:

  1. 没有办法在 React 组件或钩子之外使用上下文。为了返回缓存的数据并避免发出服务器请求,它不可能在我的 api 模块中执行。

  2. 我将敏感数据保存在依赖于当前用户会话的上下文中(例如 isFollowed)。因此,当用户注销时,上下文将被卸载(受保护的路由)。另一方面,我的 usersCache 模块仍然存在,没有敏感数据。

这是我的 CurrentUserContext 的示例(因为简单,我在这里没有使用减速器,但在我的 OtherUsersContext 中,因为状态管理很复杂,所以我这样做了):

import React, { createContext } from "react";

import useDidUpdateEffect from "../../hooks/useDidUpdateEffect";
import useStateWithCallback from "../../hooks/useStateWithCallback";
import { usersCache } from "../../services/firebase/api/users";

const CurrentUserContext = createContext(null);

export default CurrentUserContext;

export function CurrentUserProvider({ children }) {
  const [data, setData] = useStateWithCallback(undefined);

  const updateData = (data, merge = true, callback = undefined) => {
    if (merge) {
      setData((prevData) => ({ ...prevData, ...data }), callback);
    } else {
      setData(data, callback);
    }
  };

  useDidUpdateEffect(() => {
    console.log(JSON.stringify(data, null, 2));
    /*
      As the current user data is not sensitive, we can
      synchronize the users cache here.
    */
    usersCache.updateCachedUser(data.id, data);
  }, [JSON.stringify(data)]); // TODO - Avoid unnecessary executions -> deepCompare ?

  return (
    <CurrentUserContext.Provider
      value={{
        data,
        setData,
        updateData,
      }}
    >
      {children}
    </CurrentUserContext.Provider>
  );
}

为了避免运行多个 useDidUpdateEffects,我正在对用户数据进行字符串化。但是,由于它是复杂且嵌套的:

 userData = {
     avatar: {
         uri,
         thumbnailUri, 
     },
     ...
 }

在数据没有变化时执行效果,因为接收到相同的数据是乱序的:

userData = {
     avatar: {
         thumbnailUri,
         uri, 
     },
     ...
 }

顶级字段不乱。

【问题讨论】:

  • 您尝试做的事情有很多可怕和错误的地方。 JSON.stringify(complexNestedObject) 是一个巨大的危险信号,表明这里的某些事情已经脱轨。你的目标是什么?你想解决什么问题?不是,“我创建了这些奇怪的不可维护的钩子,我需要修复它们”——而是为什么你首先创建了这些奇怪的不可维护的钩子?老实说,这听起来像是你创造了它们,因为你只是没有理解正确的反应。
  • 您可能采取了一条非常困难的道路,这将使您的代码无法维护。 react 提供的 useEffect hook 实际上涵盖了很多你不需要超越的用例。也许,如果您详细说明您需要做的事情,社区会帮助您找到更简单的解决方案。
  • @Adam 我有一个上下文提供程序,它提供了更新有状态用户数据的方法。我正在使用减速器......所以没有办法等待调度,因为它没有返回承诺。所以......因为在纯函数中执行副作用是一种反模式,并且用户数据可能会因为很多情况而改变(拉动刷新,前端动作等)我决定使用(为了避免长长的 useEffects 列表)一个独特的 useDidUpdateEffect,当任何用户数据字段更改时运行 common 副作用。
  • 常见的副作用是更新我的内存用户缓存(不依赖于当前用户会话),以便在双方都有最新的数据,我的 LRU 缓存和我当前的用户上下文。
  • @Raul - 我几乎不明白这些。如果您 thunk 它绝对可以等待调度,请参阅 medium.com/solute-labs/…。但我仍然不明白它的其余部分。不过,我确实敦促你,无论你在做什么,你都过于复杂了,应该退后一步。也许use-context-selector 也可以在这里帮助你

标签: javascript reactjs react-native react-hooks


【解决方案1】:

我认为(我可能是错的)如果你使用这个效果,整个问题就会消失:

useEffect(() => {
  if(!data) return;

  // do some stuff 
},[data]);

【讨论】:

  • 我已经尝试过了,但似乎,当我正在执行拉动刷新(使用 firebase 客户端 sdk)然后更新状态数据时,新对象 ref 是不同的。此外,由于某种原因,服务器正在返回嵌套对象的无序数据:D SO...这似乎是我的问题的原因。无论如何,谢谢你的帮助。
  • @Raul 我感觉你可能对这里的hashcode 感兴趣。如果对象散列在下拉刷新时相同,则不要执行任何操作。
  • 感谢生成哈希码以有条件地更新状态的想法。我还使用钩子为我的问题编写了一个解决方案。它不如你的效率高,但会避免编写一些额外的代码。
【解决方案2】:

我遇到的问题是我的服务器正在重新调整带有无序字段的 JSON 数据...所以,您可以尝试当前的两种解决方案。

@Adam 的回答很好,避免使用自定义钩子。在其解决方案中,他只是用 JSON 对象生成一个哈希,并与前一个进行比较。如果哈希值相等,则不更新状态,避免运行 useEffect。

但是,这可能会让你在代码的不同部分编写一些条件并生成散列......所以,如果你不关心深入比较你的对象,你可以试试这个钩子:

import { useRef, useEffect } from "react";
import { isEqual } from "lodash";

export default function useDeepEffect(
  effect,
  deps = undefined,
  options = { ignoreFirstExecution: false }
) {
  const isFirst = useRef(true);
  const prevDeps = useRef(deps);

  useEffect(() => {
    if (!isFirst.current || !options.ignoreFirstExecution) {
      const isSame = prevDeps.current.every((obj, index) =>
        isEqual(obj, deps[index])
      );

      if (isFirst.current || !isSame) {
        effect();
      }
    }

    isFirst.current = false;
    prevDeps.current = deps;
  }, deps);
}

将选项 ignoreFirstExecution 设置为 true,您将获得与使用典型 useDidUpdateEffect 相同的行为。

  useDeepEffect(
    () => {
      usersCache.updateCachedUser(data.id, data);
    },
    [data],
    { ignoreFirstExecution: true }   
  );

【讨论】:

  • 仅供参考,eslint-plugin-react-hooks 不鼓励不明确定义 deps 数组中的依赖项的行为,并且有充分的理由 - 它导致难以推理(即错误)代码。 React 在创建工具(并制作自己的 API)以防止用户(尽可能)编写错误代码方面非常出色。
  • @Raul 我注意到您的代码进行了优化。如果您正在接收 JSON 数据(可能包含无序字段),那么在接收到它之后,您可以对其字段进行排序。目的是避免在不必要时更新您的状态。如果你看看你的主要问题,你的 useDidUpdateEffect 正在运行,因为不必要的状态更新!另一种解决方案可能是使用深度相等(lodash 的 isEqual)有条件地更新它。
  • 顺便说一句,解决您的问题并不容易。可能有很多解决方案!您使用该自定义挂钩的想法一点也不差,但您应该避免不必要的状态更新,而不是效果!
  • @VictorMolina - 这是对没有明确指出的问题的一个非常好的总结(但通过哈希码解决方案解决)。世界上所有复杂的效果都不会修复不必要的状态更新。
猜你喜欢
  • 2016-11-18
  • 1970-01-01
  • 1970-01-01
  • 2012-02-13
  • 1970-01-01
  • 1970-01-01
  • 2020-12-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多