如何在React Hooks useEffect上比较oldValues和newValues?


149

假设我有3个输入:rate,sendAmount和receiveAmount。我把那3个输入放在useEffect diffing params上。规则是:

  • 如果sendAmount更改,我计算 receiveAmount = sendAmount * rate
  • 如果receiveAmount改变了,我计算 sendAmount = receiveAmount / rate
  • 如果费率发生变化,我计算receiveAmount = sendAmount * rate时间sendAmount > 0或计算sendAmount = receiveAmount / rate时间receiveAmount > 0

这是codesandbox https://codesandbox.io/s/pkl6vn7x6j来演示此问题。

有没有一种方法可以比较oldValuesnewValues喜欢,componentDidUpdate而不是为此情况制作3个处理程序?

谢谢


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

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


componentDidUpdate应该如何帮助?您仍然需要编写这3个条件。
Estus Flask,

我添加了2种可选解决方案的答案。他们其中之一为您工作吗?
本·卡普

Answers:


208

您可以编写一个自定义钩子,以使用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])
}

但是,如果您useEffect对每个更改ID分别使用两个,则它更清晰,并且可能更好,更清晰地阅读和理解,您想分别处理它们


8
感谢您提供有关分别使用两个useEffect电话的说明。不知道您可以多次使用它!
JasonH

3
我尝试了上面的代码,但eslint警告我useEffect缺少依赖项prevAmount
Littlee,

2
由于prevAmount保留了state / props先前值的值,因此您无需将其作为使用useEffect的依赖项传递,并且可以针对特定情况禁用此警告。您可以阅读这篇文章以了解更多详细信息吗?
Shubham Khatri

喜欢这个– useRef总是可以解决的。
Fernando Rojo

@ShubhamKhatri:ref.current = value的useEffect包装的原因是什么?
curl_brackets

40

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

import { useEffect, useRef } from "react";

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

2
请注意,这在TSX文件中不起作用,const usePrevious = <T extends any>(...要使它在TSX文件中起作用,以使解释器看到<T>不是JSX,并且是通用限制
apokryfos

1
你能解释为什么<T extends {}>不起作用。它似乎为我工作,但我试图理解使用它的复杂性。
Karthikeyan_kk

.tsx文件包含jsx,这与html相同。泛型可能<T>是未封闭的组件标签。
SeedyROM

那么返回类型呢?我得到Missing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
Saba Ahang

@SabaAhang抱歉!TS将在大多数情况下使用类型推断来处理返回类型,但是如果启用了linting,我将添加一个返回类型以消除警告。
SeedyROM

25

选项1-useCompare挂钩

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

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

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

const Component = (props) => {
  ...
  const hasVal1Changed = useCompare(val1)
  const hasVal2Changed = useCompare(val2);
  useEffect(() => {
    if (hasVal1Changed ) {
      console.log("val1 has changed");
    }
    if (hasVal2Changed ) {
      console.log("val2 has changed");
    }
  });

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

演示版

选项2-值更改时运行useEffect

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

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

演示版


第二种选择对我有用。您能否指导我为什么我必须写两次useEffect?
塔伦·纳格帕

@TarunNagpal,您不一定需要使用useEffect两次。这取决于您的用例。想象一下,我们只想记录哪个val已更改。如果我们的依赖项数组中同时包含val1和val2,则它将在每次更改任何值时运行。然后,在传递的函数内部,我们必须找出更改了哪个val才能记录正确的消息。
Ben Carp

谢谢回复。当您说“在传递的函数中,我们必须找出哪个val发生了变化”。我们如何实现它。请引导
Tarun Nagpal

6

我刚刚发布了react-delta,它解决了这种情况。我认为useEffect责任太多。

职责范围

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

分手责任

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

责任一

责任#2

以我的经验,这种方法比useEffect/ useRef解决方案更加灵活,干净和简洁。


正是我想要的。要尝试一下!
hackerl33t

看起来真的很好,但是有点矫kill过正吗?
Hisato

@Hisato很好,可能是矫kill过正。这是一个实验性的API。坦率地说,我在团队中没有使用太多,因为它尚未广为人知或采用。从理论上讲,这听起来不错,但实际上却不值得。
奥斯丁·马勒巴

3

由于状态不会与功能组件中的组件实例紧密耦合,因此,如果不先useEffect保存状态,则无法进入先前的状态useRef。这也意味着状态更新可能在错误的地方被错误地实现了,因为以前的状态在setStateupdater函数中可用。

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

这是一个示例,看起来可能像这样:

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。您对此有什么解决方案吗?
rwinzhang

你什么意思?里面的handleChange?“ API”是否意味着远程API端点?您可以从答案中更新带有详细信息的codeandbox吗?
Estus Flask,

从更新中,我可以假定您要sendAmount异步获取等,而不是计算它们,对吧?这变化很大。可以这样做,useReduce但可能很棘手。如果您在不了解Redux的情况下就已经对Redux进行了处理,那么您可能会知道异步操作并不简单。github.com/reduxjs/redux-thunk是Redux的流行扩展,它允许异步动作。这是一个演示,它使用相同的模式(useThunkReducer),codesandbox.io / s / 6z4r79ymwr扩展了useReducer。请注意,调度功能为async,您可以在其中进行请求(注释)。
Estus Flask,

1
问题的问题是,您询问了不是您实际情况的另一件事,然后进行了更改,所以现在这是一个非常不同的问题,而现有答案似乎就好像他们只是忽略了该问题一样。不建议您在SO上这样做,因为它无法帮助您获得所需的答案。请不要从已经回答的问题中删除重要部分(计算公式)。如果你还有问题(我之前的评论之后(你可能做的),考虑提出一个新的问题,反映了你的情况,并链接到这一个作为你的一次尝试。
Estus瓶

大家好,我想这个codeandbox codesandbox.io/s/6z4r79ymwr还没有更新。我将转回该问题,对此表示抱歉。
rwinzhang

3

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

我们来看一下这种情况usePrevious,有人在此之前发表过评论:

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

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


1
React有这个例子,但是当您关心旧的道具时,他们使用的是state...而不是props...,这样就会发生错误。
santomegonzalo

3

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

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

}

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


1
这只能使用一次,连续后prop的改变,你不能~ do stuff here ~
KarolisŠarapnickis

2
似乎您应该setHasPropChanged(false)在末尾调用~ do stuff here ~以“重置”状态。(但这会在重新渲染后重新设置)
Kevin Wang,

感谢您的反馈,您说的对,就是最新的解决方案
Charlie

@ AntonioPavicevac-Ortiz我已经更新了答案,现在将propHasChanged渲染为true,然后在渲染时将其调用一次,这可能是一个更好的解决方案,只是剔除useEffect并检查该道具
Charlie Tupman

我认为我原来的用法已经丢失。回顾一下代码,您可以只使用useEffect
Charlie

2

从公认的答案出发,这是不需要自定义钩子的替代解决方案:

const Component = (props) => {
  const { receiveAmount, sendAmount } = props;
  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]);
};

1
好的解决方案,但包含语法错误。 useRefuserRef。忘记使用电流prevAmount.current
Blake Plumb,

0

这是我使用的自定义钩子,我相信它比使用更直观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])

希望有帮助。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.