'How to compare oldValues and newValues on React Hooks useEffect?
Let's say I have 3 inputs: rate, sendAmount, and receiveAmount. I put that 3 inputs on useEffect diffing params. The rules are:
- If sendAmount changed, I calculate
receiveAmount = sendAmount * rate
- If receiveAmount changed, I calculate
sendAmount = receiveAmount / rate
- If rate changed, I calculate
receiveAmount = sendAmount * rate
whensendAmount > 0
or I calculatesendAmount = receiveAmount / rate
whenreceiveAmount > 0
Here is the codesandbox https://codesandbox.io/s/pkl6vn7x6j to demonstrate the problem.
Is there a way to compare the oldValues
and newValues
like on componentDidUpdate
instead of making 3 handlers for this case?
Thanks
Here is my final solution with usePrevious
https://codesandbox.io/s/30n01w2r06
In this case, I cannot use multiple useEffect
because each change is leading to the same network call. That's why I also use changeCount
to track the change too. This changeCount
also helpful to track changes from local only, so I can prevent unnecessary network call because of changes from the server.
Solution 1:[1]
You can write a custom hook to provide you a previous props using useRef
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
and then use it in 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])
}
However its clearer and probably better and clearer to read and understand if you use two useEffect
separately for each change id you want to process them separately
Solution 2:[2]
Incase anybody is looking for a TypeScript version of usePrevious:
In a .tsx
module:
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;
};
Or in a .ts
module:
import { useEffect, useRef } from "react";
const usePrevious = <T>(value: T): T | undefined => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
Solution 3:[3]
Option 1 - run useEffect when value changes
const Component = (props) => {
useEffect(() => {
console.log("val1 has changed");
}, [val1]);
return <div>...</div>;
};
Option 2 - useHasChanged hook
Comparing a current value to a previous value is a common pattern, and justifies a custom hook of it's own that hides implementation details.
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;
}
Solution 4:[4]
Going off the accepted answer, an alternative solution that doesn't require a custom hook:
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]);
};
This assumes you actually need reference to the previous values for anything in the "process here" bits. Otherwise unless your conditionals are beyond a straight !==
comparison, the simplest solution here would just be:
const Component = ({ receiveAmount, sendAmount }) => {
useEffect(() => {
// process here
}, [receiveAmount]);
useEffect(() => {
// process here
}, [sendAmount]);
};
Solution 5:[5]
I just published react-delta which solves this exact sort of scenario. In my opinion, useEffect
has too many responsibilities.
Responsibilities
- It compares all values in its dependency array using
Object.is
- It runs effect/cleanup callbacks based on the result of #1
Breaking Up Responsibilities
react-delta
breaks useEffect
's responsibilities into several smaller hooks.
Responsibility #1
usePrevious(value)
useLatest(value)
useDelta(value, options)
useDeltaArray(valueArray, options)
useDeltaObject(valueObject, options)
some(deltaArray)
every(deltaArray)
Responsibility #2
In my experience, this approach is more flexible, clean, and concise than useEffect
/useRef
solutions.
Solution 6:[6]
If you prefer a useEffect
replacement approach:
const usePreviousEffect = (fn, inputs = []) => {
const previousInputsRef = useRef([...inputs])
useEffect(() => {
fn(previousInputsRef.current)
previousInputsRef.current = [...inputs]
}, inputs)
}
And use it like this:
usePreviousEffect(
([prevReceiveAmount, prevSendAmount]) => {
if (prevReceiveAmount !== receiveAmount) // side effect here
if (prevSendAmount !== sendAmount) // side effect here
},
[receiveAmount, sendAmount]
)
Note that the first time the effect executes, the previous values passed to your fn
will be the same as your initial input values. This would only matter to you if you wanted to do something when a value did not change.
Solution 7:[7]
Since state isn't tightly coupled with component instance in functional components, previous state cannot be reached in useEffect
without saving it first, for instance, with useRef
. This also means that state update was possibly incorrectly implemented in wrong place because previous state is available inside setState
updater function.
This is a good use case for useReducer
which provides Redux-like store and allows to implement respective pattern. State updates are performed explicitly, so there's no need to figure out which state property is updated; this is already clear from dispatched action.
Here's an example what it may look like:
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
});
...
Solution 8:[8]
Here's a custom hook that I use which I believe is more intuitive than using 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)
}
You'd use useTransition
as follows.
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])
Hope that helps.
Solution 9:[9]
For really simple prop comparison you can use useEffect
to easily check to see if a prop has updated.
const myComponent = ({ prop }) => {
useEffect(() => {
---Do stuffhere----
}, [prop])
}
useEffect
will then only run your code if the prop changes.
Solution 10:[10]
Using Ref will introduce a new kind of bug into the app.
Let's see this case using usePrevious
that someone commented before:
- prop.minTime: 5 ==> ref.current = 5 | set ref.current
- prop.minTime: 5 ==> ref.current = 5 | new value is equal to ref.current
- prop.minTime: 8 ==> ref.current = 5 | new value is NOT equal to ref.current
- prop.minTime: 5 ==> ref.current = 5 | new value is equal to ref.current
As we can see here, we are not updating the internal ref
because we are using useEffect
Solution 11:[11]
Be careful with most voted answers. For more complex scenarios above variations of usePrevious
can give you too much re-renders (1) or the same value as original (2).
We have to:
- Add
[value]
as dependency inuseEffect
to re-run only ifvalue
changes - Assign
JSON.parse(JSON.stringify(value))
(or some deep copy) toref.current
insindeuseEffect
to prevent passing the reference of state to ref instead of the value
Upgraded hook:
const usePrevious = <T>(value: T): T => {
const ref: any = useRef<T>()
useEffect(() => {
ref.current = JSON.parse(JSON.stringify(value))
}, [value])
return ref.current
}
Solution 12:[12]
You can use useImmer opposed to useState and access the state. Example: https://css-tricks.com/build-a-chat-app-using-react-hooks-in-100-lines-of-code/
Solution 13:[13]
I did not like any of the answers above, I wanted the ability to pass an array of booleans and if one of them is true so rerender
/**
* 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]);
Solution 14:[14]
In your case(simple object):
useEffect(()=>{
// your logic
}, [rate, sendAmount, receiveAmount])
In other case(complex object)
const {cityInfo} = props;
useEffect(()=>{
// some logic
}, [cityInfo.cityId])
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow