如何比较反应钩子useEffect上的oldValues和newValues ?

假设我有3个输入:rate、sendAmount和receiveAmount。我把这3个输入放到useEffect上。规则如下:

  • 如果sendAmount改变了,我计算receiveAmount = sendAmount * rate
  • 如果receiveAmount改变了,我计算sendAmount = receiveAmount / rate
  • 如果利率改变,我计算receiveAmount = sendAmount * ratesendAmount > 0或我计算sendAmount = receiveAmount / ratereceiveAmount > 0

下面是代码和框https://codesandbox.io/s/pkl6vn7x6j来演示这个问题。

是否有一种方法可以比较oldValuesnewValues,就像在componentDidUpdate上一样,而不是为这种情况创建3个处理程序?

谢谢


这是我的最终解决方案与usePrevious https://codesandbox.io/s/30n01w2r06 < / p >

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

343511 次浏览

你可以编写一个自定义钩子来提供之前的道具使用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,它更清晰,可能更好,更清楚地阅读和理解

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

这是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
});
...

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

const Component = (props) => {


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


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

< a href = " https://codesandbox。io /嵌入/ optimistic-sky-0x0b2 noreferrer“rel = >演示< / >

选项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;
}




< a href = " https://codesandbox。io /嵌入/ vigilant-lake-8fxxs noreferrer“rel = >演示< / >

如果有人正在寻找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;
};

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

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

useEffect将只在道具改变时运行你的代码。

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

让我们看看这个使用usePrevious的例子,之前有人注释过:

  1. 道具。minTime: 5 ==> ref.current = 5 | set ref.current
  2. 道具。minTime: 5 ==> ref.current = 5 |的新值等于ref.current
  3. 道具。minTime: 8 ==> ref.current = 5 |的新值不等于ref.current
  4. 道具。minTime: 5 ==> ref.current = 5 |的新值等于ref.current

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

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

责任

  1. 它使用Object.is比较其依赖数组中的所有值
  2. 它基于#1的结果运行effect/cleanup回调

分解责任

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

责任# 1

责任# 2

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

离开公认的答案,一个不需要自定义钩子的替代解决方案:

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]);
};

下面是我使用的一个自定义钩子,我认为它比使用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])

希望这能有所帮助。

如果你更喜欢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的值将与你的初始输入值相同。当值没有发生变化时,这只会对你有影响。

你可以使用useImmer代替useState来访问状态。 例如:https://css-tricks.com/build-a-chat-app-using-react-hooks-in-100-lines-of-code/ < / p >

在你的例子中(简单对象):

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

在其他情况下(复杂对象)

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

我不喜欢上面的任何答案,我想要传递一个布尔值数组的能力,如果其中一个是真的,那么重新渲染

/**
* 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]);

小心大多数投票的答案。对于上述更复杂的场景,usePrevious的变化可能会给你太多的重渲染(1)或与原始(2)相同的值。

我们必须:

  1. useEffect中添加[value]作为依赖项,仅在value发生变化时重新运行
  2. JSON.parse(JSON.stringify(value))(或一些深层拷贝)赋值给useEffect中的ref.current,以防止将state的引用传递给ref而不是值

升级看点:

const usePrevious = <T>(value: T): T => {
const ref: any = useRef<T>()


useEffect(() => {
ref.current = JSON.parse(JSON.stringify(value))
}, [value])


return ref.current
}

下面是Typescript版本Aadit M Shah的回答

我将它从useTransition重命名为usePrevious,因为useTransition已经存在于React中。

import { useEffect, useRef, useState } from 'react';


const usePrevious = <T extends any[],>(callback: (prev: T) => void, deps: T): void => {
const callbackRef = useRef<null | ((prev: T) => void)>(null);


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


const depsRef = useRef<null | T>(null);


const [initial, setInitial] = useState(true);


useEffect(() => {
if (!initial && depsRef.current !== null && callbackRef.current !== null) {
callbackRef.current(depsRef.current);
}


depsRef.current = deps;
setInitial(false);
}, deps);
}


export default usePrevious;

用法:

  usePrevious<[boolean]>(([prevIsOpen]) => {
console.log('prev', prevIsOpen);
console.log('now', isOpen);
}, [isOpen])