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 when sendAmount > 0 or I calculate sendAmount = receiveAmount / rate when receiveAmount > 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.
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
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;
};
const usePrevious = <T extends any>(...
to make the interpreter see think that <T> is not JSX and is a generic restriction
<T extends {}>
will not work. It seems to be working for me but I am trying to understand the complexity of using it like that.
.ts
file. If you extend {}
you will get errors if you skip specifying T in usePrevious<T>
.
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;
}
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]);
};
useRef
not userRef
. Forgot to use current prevAmount.current
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
useConditionalEffect(callback, boolean)
In my experience, this approach is more flexible, clean, and concise than useEffect
/useRef
solutions.
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.
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
});
...
sendAmount
, etc asynchronously instead of calculating them, right? This changes a lot. It's possible to do this with useReduce
but may be tricky. If you dealt with Redux before you may know that async operations aren't straightforward there. github.com/reduxjs/redux-thunk is a popular extension for Redux that allows for async actions . Here's a demo that augments useReducer with same pattern (useThunkReducer), codesandbox.io/s/6z4r79ymwr . Notice that dispatched function is async
, you can do requests there (commented).
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.
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.
prop
changes you will not be able to ~ do stuff here ~
setHasPropChanged(false)
at the end of ~ do stuff here ~
to "reset" your state. (But this would reset in an extra rerender)
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
state
... and not the props
... when you care about the old props then error will happened.
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 in useEffect to re-run only if value changes Assign JSON.parse(JSON.stringify(value)) (or some deep copy) to ref.current insinde useEffect 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
}
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/
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]);
Here's a Typescript version Aadit M Shah's Answer.
I renamed it from useTransition
to usePrevious
since useTransition
already exists in 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;
Usage:
usePrevious<[boolean]>(([prevIsOpen]) => {
console.log('prev', prevIsOpen);
console.log('now', isOpen);
}, [isOpen])
In your case(simple object):
useEffect(()=>{
// your logic
}, [rate, sendAmount, receiveAmount])
In other case(complex object)
const {cityInfo} = props;
useEffect(()=>{
// some logic
}, [cityInfo.cityId])
Success story sharing
useEffect
calls separately. Wasn't aware you could use it multiple times!useEffect
missing dependenciesprevAmount
usePrevious
, shouldn't theuseEffect
have a dependency onvalue
? Otherwise if the component is re-rendered due to a different state change, in the next render,previousValue
will equalvalue
, right? Or am I missing something?react
becomes more and more awful: people are usinguseRef
just for receiving previous props (!) and don't care how much it will cost.