【问题标题】:How to compare oldValues and newValues on React Hooks useEffect?如何比较 React Hooks useEffect 上的 oldValues 和 newValues?
【发布时间】:2019-04-26 00:38:30
【问题描述】:

假设我有 3 个输入:rate、sendAmount 和 receiveAmount。我将这 3 个输入放在 useEffect 差异参数上。规则是:

  • 如果 sendAmount 发生变化,我计算 receiveAmount = sendAmount * rate
  • 如果receiveAmount改变,我计算sendAmount = receiveAmount / rate
  • 如果汇率发生变化,我在 sendAmount > 0 时计算 receiveAmount = sendAmount * rate,或者在 receiveAmount > 0 时计算 sendAmount = receiveAmount / rate

这里是代码框https://codesandbox.io/s/pkl6vn7x6j 来演示问题。

有没有办法比较componentDidUpdate 上的oldValuesnewValues,而不是为这种情况制作3 个处理程序?

谢谢


这是我使用usePrevious 的最终解决方案 https://codesandbox.io/s/30n01w2r06

在这种情况下,我不能使用多个useEffect,因为每次更改都会导致相同的网络调用。这就是为什么我也使用changeCount 来跟踪更改。这个changeCount 也有助于仅从本地跟踪更改,因此我可以防止由于服务器更改而导致不必要的网络调用。

【问题讨论】:

  • componentDidUpdate 究竟应该如何提供帮助?您仍然需要编写这 3 个条件。
  • 我添加了一个带有 2 个可选解决方案的答案。其中一个对你有用吗?

标签: reactjs react-hooks


【解决方案1】:

您可以编写一个自定义挂钩来为您提供previous props using useRef

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

然后在useEffect中使用它

const Component = (props) => {
    const {receiveAmount, sendAmount } = props
    const prevAmount = usePrevious({receiveAmount, sendAmount});
    useEffect(() => {
        if(prevAmount.receiveAmount !== receiveAmount) {

         // process here
        }
        if(prevAmount.sendAmount !== sendAmount) {

         // process here
        }
    }, [receiveAmount, sendAmount])
}

但是,如果您对要单独处理的每个更改 ID 分别使用两个 useEffect,它会更清晰,可能更易于阅读和理解

【讨论】:

  • 感谢您关于分别使用两个 useEffect 呼叫的说明。不知道你可以多次使用它!
  • 我尝试了上面的代码,但 eslint 警告我 useEffect 缺少依赖项 prevAmount
  • 由于 prevAmount 持有 state/props 的前一个值的值,因此您无需将其作为依赖项传递给 useEffect,您可以针对特定情况禁用此警告。您可以阅读这篇文章了解更多详情?
  • 喜欢这个——useRef 总是能帮上忙。
  • usePrevious中,useEffect不应该依赖value吗?否则,如果组件由于不同的状态更改而重新渲染,在下一次渲染中,previousValue 将等于 value,对吗?还是我错过了什么?
【解决方案2】:

由于状态与功能组件中的组件实例没有紧密耦合,因此如果不先保存 useEffect 就无法达到先前的状态,例如 useRef。这也意味着状态更新可能在错误的地方被错误地执行,因为之前的状态在 setState 更新函数中可用。

这是useReducer 的一个很好的用例,它提供了类似 Redux 的存储并允许实现各自的模式。状态更新是显式执行的,因此无需确定更新了哪个状态属性;这从调度的动作中已经很清楚了。

这是example 的样子:

function reducer({ sendAmount, receiveAmount, rate }, action) {
  switch (action.type) {
    case "sendAmount":
      sendAmount = action.payload;
      return {
        sendAmount,
        receiveAmount: sendAmount * rate,
        rate
      };
    case "receiveAmount":
      receiveAmount = action.payload;
      return {
        sendAmount: receiveAmount / rate,
        receiveAmount,
        rate
      };
    case "rate":
      rate = action.payload;
      return {
        sendAmount: receiveAmount ? receiveAmount / rate : sendAmount,
        receiveAmount: sendAmount ? sendAmount * rate : receiveAmount,
        rate
      };
    default:
      throw new Error();
  }
}

function handleChange(e) {
  const { name, value } = e.target;
  dispatch({
    type: name,
    payload: value
  });
}

...
const [state, dispatch] = useReducer(reducer, {
  rate: 2,
  sendAmount: 0,
  receiveAmount: 0
});
...

【讨论】:

  • 嗨@estus,感谢这个想法。它给了我另一种思考方式。但是,我忘了提到我需要为每个案例调用 API。你有什么解决办法吗?
  • 什么意思?内部handleChange? “API”是指远程 API 端点吗?您可以从答案中更新代码和框的详细信息吗?
  • 从更新中我可以假设您想异步获取sendAmount 等而不是计算它们,对吧?这变化很大。使用useReduce 可以做到这一点,但可能会很棘手。如果您在处理过 Redux 之前可能会知道异步操作在那里并不简单。 github.com/reduxjs/redux-thunk 是 Redux 的一个流行扩展,它允许异步操作。这是一个使用相同模式 (useThunkReducer) 扩充 useReducer 的演示,codesandbox.io/s/6z4r79ymwr。注意dispatch的函数是async,你可以在那里做请求(注释)。
  • 这个问题的问题是你问了一个不是你真实情况的不同的事情然后改变了它,所以现在这是一个非常不同的问题,而现有的答案似乎只是忽略了这个问题.这不是关于 SO 的推荐做法,因为它不能帮助您获得所需的答案。请不要从答案已经提到的问题中删除重要部分(计算公式)。如果您仍然有问题(在我之前的评论之后(您可能会这样做),请考虑提出一个反映您的案例的新问题并链接到此问题作为您之前的尝试。
  • 嗨 estus,我认为这个代码框 codesandbox.io/s/6z4r79ymwr 尚未更新。我会改回问题,抱歉。
【解决方案3】:

选项 1 - 当值改变时运行 useEffect

const Component = (props) => {

  useEffect(() => {
    console.log("val1 has changed");
  }, [val1]);

  return <div>...</div>;
};

Demo

选项 2 - useHasChanged 挂钩

将当前值与前一个值进行比较是一种常见模式,并证明了它自己的自定义钩子隐藏了实现细节。

const Component = (props) => {
  const hasVal1Changed = useHasChanged(val1)

  useEffect(() => {
    if (hasVal1Changed ) {
      console.log("val1 has changed");
    }
  });

  return <div>...</div>;
};

const useHasChanged= (val: any) => {
    const prevVal = usePrevious(val)
    return prevVal !== val
}

const usePrevious = (value) => {
    const ref = useRef();
    useEffect(() => {
      ref.current = value;
    });
    return ref.current;
}


Demo

【讨论】:

  • 第二个选项对我有用。你能指导我为什么我必须写 useEffect Twice 吗?
  • @TarunNagpal,您不一定需要使用 useEffect 两次。这取决于您的用例。想象一下,我们只想记录哪个 val 发生了变化。如果我们的依赖数组中同时包含 val1 和 val2,则每次任何值发生更改时都会运行它。然后在我们传递的函数中,我们必须找出哪个 val 发生了变化来记录正确的消息。
  • 感谢您的回复。当您说“在我们传递的函数内部,我们必须找出哪个 val 发生了变化”。我们如何才能实现它。请指导
  • 谢谢,选项 2 - 正是我所需要的
【解决方案4】:

如果有人正在寻找 usePrevious 的 TypeScript 版本:

.tsx 模块中:

import { useEffect, useRef } from "react";

const usePrevious = <T extends unknown>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

或者在.ts 模块中:

import { useEffect, useRef } from "react";

const usePrevious = <T>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

【讨论】:

  • 请注意,这在 TSX 文件中不起作用,要使其在 TSX 文件中起作用,请更改为 const usePrevious = &lt;T extends any&gt;(... 以使解释器认为 不是 JSX 并且是通用的限制
  • 你能解释一下为什么&lt;T extends {}&gt; 不起作用。它似乎对我有用,但我试图了解这样使用它的复杂性。
  • 最好扩展未知,或者简单地将钩子放在.ts 文件中。如果您扩展 {},如果您跳过在 usePrevious&lt;T&gt; 中指定 T,则会出现错误。
  • @fgblomqvist 更新了我的答案。再次感谢您的反馈。
  • 要使其在 TSX 文件中工作,您还可以编写 以区分 JSX 语法(关注尾随逗号)
【解决方案5】:

对于非常简单的道具比较,您可以使用useEffect 轻松检查道具是否已更新。

const myComponent = ({ prop }) => {
  useEffect(() => {
    ---Do stuffhere----
  }, [prop])
}

useEffect 将仅在 prop 更改时运行您的代码。

【讨论】:

  • 这只能工作一次,在连续的prop更改后你将无法~ do stuff here ~
  • 看来你应该在~ do stuff here ~ 的末尾调用setHasPropChanged(false) 来“重置”你的状态。 (但这会在额外的重新渲染中重置)
  • 感谢您的反馈,您说得对,已更新解决方案
  • @AntonioPavicevac-Ortiz 我已经更新了答案,现在将 propHasChanged 渲染为 true,然后在渲染时调用它一次,这可能是一个更好的解决方案,只是去掉 useEffect 并检查 prop
  • 我认为我原来使用这个已经丢失了。回顾代码你可以只使用 useEffect
【解决方案6】:

使用 Ref 会在应用程序中引入一种新的错误。

让我们使用之前有人评论过的usePrevious 来看看这个案例:

  1. prop.minTime: 5 ==> ref.current = 5 |设置参考电流
  2. prop.minTime: 5 ==> ref.current = 5 |新值等于 ref.current
  3. prop.minTime: 8 ==> ref.current = 5 |新值不等于 ref.current
  4. prop.minTime: 5 ==> ref.current = 5 |新值等于 ref.current

正如我们在这里看到的,我们没有更新内部的ref,因为我们使用的是useEffect

【讨论】:

  • React 有这个例子,但他们使用的是state... 而不是props... 当你关心旧的道具时会发生错误。
【解决方案7】:

我刚刚发布了react-delta,它解决了这种确切的情况。在我看来,useEffect 的职责太多了。

职责

  1. 它使用Object.is比较其依赖数组中的所有值
  2. 它根据 #1 的结果运行效果/清理回调

分拆责任

react-deltauseEffect 的职责分解为几个较小的钩子。

职责#1

职责 #2

根据我的经验,这种方法比useEffect/useRef 解决方案更灵活、更简洁、更简洁。

【讨论】:

  • 正是我想要的。试试看!
  • @Hisato 这很可能是矫枉过正。这是一个实验性的 API。坦率地说,我没有在我的团队中大量使用它,因为它并不广为人知或采用。从理论上讲,这听起来不错,但在实践中可能不值得。
【解决方案8】:

放弃公认的答案,一种不需要自定义挂钩的替代解决方案:

const Component = ({ receiveAmount, sendAmount }) => {
  const prevAmount = useRef({ receiveAmount, sendAmount }).current;
  useEffect(() => {
    if (prevAmount.receiveAmount !== receiveAmount) {
     // process here
    }
    if (prevAmount.sendAmount !== sendAmount) {
     // process here
    }
    return () => { 
      prevAmount.receiveAmount = receiveAmount;
      prevAmount.sendAmount = sendAmount;
    };
  }, [receiveAmount, sendAmount]);
};

这假设您实际上需要参考“此处处理”位中的任何内容的先前值。否则,除非您的条件超出了直接的 !== 比较,否则这里最简单的解决方案就是:

const Component = ({ receiveAmount, sendAmount }) => {
  useEffect(() => {
     // process here
  }, [receiveAmount]);

  useEffect(() => {
     // process here
  }, [sendAmount]);
};

【讨论】:

  • 很好的解决方案,但包含语法错误。 useRef 不是 userRef。忘记使用当前的prevAmount.current
  • 这太酷了,如果我能投票更多,我会的!完全有道理,我最初的答案来自一个无知的地方。我认为这可能是迄今为止我见过的最简单、最优雅的模式。
  • 你可以进一步抽象它来制作一个超级简单的实用程序。
  • 这将为您提供 initial 值,而不是 previous 值。 usePrevious() 钩子起作用的原因是它被包裹在 useEffect()
  • @Justin 返回函数应该在每次渲染时更新之前的值。你测试过吗?
【解决方案9】:

这是我使用的自定义钩子,我认为它比使用 usePrevious 更直观。

import { useRef, useEffect } from 'react'

// useTransition :: Array a => (a -> Void, a) -> Void
//                              |_______|  |
//                                  |      |
//                              callback  deps
//
// The useTransition hook is similar to the useEffect hook. It requires
// a callback function and an array of dependencies. Unlike the useEffect
// hook, the callback function is only called when the dependencies change.
// Hence, it's not called when the component mounts because there is no change
// in the dependencies. The callback function is supplied the previous array of
// dependencies which it can use to perform transition-based effects.
const useTransition = (callback, deps) => {
  const func = useRef(null)

  useEffect(() => {
    func.current = callback
  }, [callback])

  const args = useRef(null)

  useEffect(() => {
    if (args.current !== null) func.current(...args.current)
    args.current = deps
  }, deps)
}

您可以使用useTransition,如下所示。

useTransition((prevRate, prevSendAmount, prevReceiveAmount) => {
  if (sendAmount !== prevSendAmount || rate !== prevRate && sendAmount > 0) {
    const newReceiveAmount = sendAmount * rate
    // do something
  } else {
    const newSendAmount = receiveAmount / rate
    // do something
  }
}, [rate, sendAmount, receiveAmount])

希望对您有所帮助。

【讨论】:

    【解决方案10】:

    如果您更喜欢useEffect 替换方法:

    const usePreviousEffect = (fn, inputs = []) => {
      const previousInputsRef = useRef([...inputs])
    
      useEffect(() => {
        fn(previousInputsRef.current)
        previousInputsRef.current = [...inputs]
      }, inputs)
    }
    

    并像这样使用它:

    usePreviousEffect(
      ([prevReceiveAmount, prevSendAmount]) => {
        if (prevReceiveAmount !== receiveAmount) // side effect here
        if (prevSendAmount !== sendAmount) // side effect here
      },
      [receiveAmount, sendAmount]
    )
    

    请注意,在效果执行的第一次时,之前传递给fn 的值将与您的初始输入值相同。只有当值没有发生变化时您想做某事,这对您才有意义。

    【讨论】:

      【解决方案11】:

      您可以使用 useImmer 与 useState 相反并访问状态。 示例:https://css-tricks.com/build-a-chat-app-using-react-hooks-in-100-lines-of-code/

      【讨论】:

        【解决方案12】:

        在你的情况下(简单对象):

        useEffect(()=>{
          // your logic
        }, [rate, sendAmount, receiveAmount])
        

        其他情况(复杂对象)

        const {cityInfo} = props;
        useEffect(()=>{
          // some logic
        }, [cityInfo.cityId])
        

        【讨论】:

          【解决方案13】:

          我不喜欢上面的任何答案,我希望能够传递一组布尔值,如果其中一个为真,那么重新渲染

          /**
           * effect fires if one of the conditions in the dependency array is true
           */
          export const useEffectCompare = (callback: () => void, conditions: boolean[], effect = useEffect) => {
            const shouldUpdate = useRef(false);
            if (conditions.some((cond) => cond)) shouldUpdate.current = !shouldUpdate.current;
            effect(callback, [shouldUpdate.current]);
          };
          
          //usage - will fire because one of the dependencies is true.
          useEffectCompare(() => {
            console.log('test!');
          }, [false, true]);
          

          【讨论】:

            猜你喜欢
            • 2021-11-13
            • 1970-01-01
            • 2020-03-23
            • 2020-06-08
            • 2021-03-30
            • 2020-11-01
            • 2020-01-24
            • 2020-05-07
            • 2021-11-11
            相关资源
            最近更新 更多