如何比较 React Hooks useEffect 上的旧值和新值?
How to compare oldValues and newValues on React Hooks useEffect?
假设我有 3 个输入:rate、sendAmount 和 receiveAmount。我将这 3 个输入放在 useEffect 差异参数上。规则是:
- 如果sendAmount改变了,我计算
receiveAmount = sendAmount * rate
- 如果receiveAmount改变了,我计算
sendAmount = receiveAmount / rate
- 如果汇率改变了,我在
sendAmount > 0
时计算 receiveAmount = sendAmount * rate
或在 receiveAmount > 0
时计算 sendAmount = receiveAmount / rate
这里是 codesandbox https://codesandbox.io/s/pkl6vn7x6j 来演示这个问题。
有没有办法像 componentDidUpdate
那样比较 oldValues
和 newValues
而不是为这种情况制作 3 个处理程序?
谢谢
这是我使用 usePrevious
的最终解决方案
https://codesandbox.io/s/30n01w2r06
在这种情况下,我不能使用多个 useEffect
,因为每次更改都会导致相同的网络调用。这就是为什么我也使用 changeCount
来跟踪更改。这 changeCount
也有助于仅跟踪本地更改,因此我可以防止由于服务器更改而导致的不必要的网络调用。
您可以编写自定义挂钩来为您提供 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
中无法在不先保存的情况下达到先前的状态,例如,使用 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
});
...
选项 1 - 运行 当值改变时使用 Effect
const Component = (props) => {
useEffect(() => {
console.log("val1 has changed");
}, [val1]);
return <div>...</div>;
};
选项 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;
}
如果有人正在寻找 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
来看这个案例:
- prop.minTime: 5 ==> ref.current = 5 |设置 ref.current
- prop.minTime: 5 ==> ref.current = 5 |新值等于 ref.current
- prop.minTime: 8 ==> ref.current = 5 |新值不等于 ref.current
- prop.minTime: 5 ==> ref.current = 5 |新值等于 ref.current
正如我们在这里看到的,我们没有更新内部 ref
因为我们正在使用 useEffect
我刚刚发布了 react-delta 解决了这种情况。在我看来,useEffect
的责任太多了。
职责
- 它使用
Object.is
比较其依赖数组中的所有值
- 它根据 #1
的结果运行 effect/cleanup 回调
分手责任
react-delta
将 useEffect
的职责分解为几个较小的钩子。
责任#1
usePrevious(value)
useLatest(value)
useDelta(value, options)
useDeltaArray(valueArray, options)
useDeltaObject(valueObject, options)
some(deltaArray)
every(deltaArray)
责任#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/
你的情况(简单对象):
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
的上述变体可能会给您太多 re-renders (1) 或与原始值 (2) 相同的值。
我们必须:
- 仅当
value
更改 时,将 [value]
作为 useEffect
中的依赖项添加到 re-run
- 将
JSON.parse(JSON.stringify(value))
(或一些深层副本)分配给 ref.current
ininde useEffect
以防止将状态引用传递给 ref 而不是值
升级挂钩:
const usePrevious = <T>(value: T): T => {
const ref: any = useRef<T>()
useEffect(() => {
ref.current = JSON.parse(JSON.stringify(value))
}, [value])
return ref.current
}
假设我有 3 个输入:rate、sendAmount 和 receiveAmount。我将这 3 个输入放在 useEffect 差异参数上。规则是:
- 如果sendAmount改变了,我计算
receiveAmount = sendAmount * rate
- 如果receiveAmount改变了,我计算
sendAmount = receiveAmount / rate
- 如果汇率改变了,我在
sendAmount > 0
时计算receiveAmount = sendAmount * rate
或在receiveAmount > 0
时计算
sendAmount = receiveAmount / rate
这里是 codesandbox https://codesandbox.io/s/pkl6vn7x6j 来演示这个问题。
有没有办法像 componentDidUpdate
那样比较 oldValues
和 newValues
而不是为这种情况制作 3 个处理程序?
谢谢
这是我使用 usePrevious
的最终解决方案
https://codesandbox.io/s/30n01w2r06
在这种情况下,我不能使用多个 useEffect
,因为每次更改都会导致相同的网络调用。这就是为什么我也使用 changeCount
来跟踪更改。这 changeCount
也有助于仅跟踪本地更改,因此我可以防止由于服务器更改而导致的不必要的网络调用。
您可以编写自定义挂钩来为您提供 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
中无法在不先保存的情况下达到先前的状态,例如,使用 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
});
...
选项 1 - 运行 当值改变时使用 Effect
const Component = (props) => {
useEffect(() => {
console.log("val1 has changed");
}, [val1]);
return <div>...</div>;
};
选项 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;
}
如果有人正在寻找 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
来看这个案例:
- prop.minTime: 5 ==> ref.current = 5 |设置 ref.current
- prop.minTime: 5 ==> ref.current = 5 |新值等于 ref.current
- prop.minTime: 8 ==> ref.current = 5 |新值不等于 ref.current
- prop.minTime: 5 ==> ref.current = 5 |新值等于 ref.current
正如我们在这里看到的,我们没有更新内部 ref
因为我们正在使用 useEffect
我刚刚发布了 react-delta 解决了这种情况。在我看来,useEffect
的责任太多了。
职责
- 它使用
Object.is
比较其依赖数组中的所有值
- 它根据 #1 的结果运行 effect/cleanup 回调
分手责任
react-delta
将 useEffect
的职责分解为几个较小的钩子。
责任#1
usePrevious(value)
useLatest(value)
useDelta(value, options)
useDeltaArray(valueArray, options)
useDeltaObject(valueObject, options)
some(deltaArray)
every(deltaArray)
责任#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/
你的情况(简单对象):
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
的上述变体可能会给您太多 re-renders (1) 或与原始值 (2) 相同的值。
我们必须:
- 仅当
value
更改 时,将 - 将
JSON.parse(JSON.stringify(value))
(或一些深层副本)分配给ref.current
inindeuseEffect
以防止将状态引用传递给 ref 而不是值
[value]
作为 useEffect
中的依赖项添加到 re-run
升级挂钩:
const usePrevious = <T>(value: T): T => {
const ref: any = useRef<T>()
useEffect(() => {
ref.current = JSON.parse(JSON.stringify(value))
}, [value])
return ref.current
}