Reputation: 18093
useState
always triggers an update even when the data's values haven't changed.
Here's a working demo of the problem: demo
I'm using the useState
hook to update an object and I'm trying to get it to only update when the values in that object change. Because React uses the Object.is comparison algorithm to determine when it should update; objects with equivalent values still cause the component to re-render because they're different objects.
Ex. This component will always re-render even though the value of the payload stays as { foo: 'bar' }
const UseStateWithNewObject = () => {
const [payload, setPayload] = useState({});
useEffect(
() => {
setInterval(() => {
setPayload({ foo: 'bar' });
}, 500);
},
[setPayload]
);
renderCountNewObject += 1;
return <h3>A new object, even with the same values, will always cause a render: {renderCountNewObject}</h3>;
};
Is there away that I can implement something like shouldComponentUpdate
with hooks to tell react to only re-render my component when the data changes?
Upvotes: 23
Views: 32400
Reputation: 20885
If I understand well, you are trying to only call setState
whenever the new value for the state has changed, thus preventing unnecessary rerenders when it has NOT changed.
If that is the case you can take advantage of the callback form of useState
const [state, setState] = useState({});
setState(prevState => {
// here check for equality and return prevState if the same
// If the same
return prevState; // -> NO RERENDER !
// If different
return {...prevState, ...updatedValues}; // Rerender
});
Here is a custom hook (in TypeScript) that does that for you automatically. It uses isEqual
from lodash. But feel free to replace it with whatever equality function you see fit.
import { isEqual } from 'lodash';
import { useState } from 'react';
const useMemoizedState = <T>(initialValue: T): [T, (val: T) => void] => {
const [state, _setState] = useState<T>(initialValue);
const setState = (newState: T) => {
_setState((prev) => {
if (!isEqual(newState, prev)) {
return newState;
} else {
return prev;
}
});
};
return [state, setState];
};
export default useMemoizedState;
Usage:
const [value, setValue] = useMemoizedState({ [...] });
Upvotes: 20
Reputation: 1939
I think we would need to see a better real life example of what you are tying to do, but from what you have shared I think the logic would need to move upstream to a point before the state gets set.
For example, you could manually compare the incoming values in a useEffect
before you update state, because this is basically what you are asking if React can do for you.
There is a library use-deep-compare-effect
https://github.com/kentcdodds/use-deep-compare-effect that may be of use to you in this case, taking care of a lot of the manual effort involved, but even then, this solution assumes the developer is going to manually decide (based on incoming props, etc) if the state should be updated.
So for example:
const obj = {foo: 'bar'}
const [state, setState] = useState(obj)
useEffect(() => {
// manually deep compare here before updating state
if(obj.foo === state.foo) return
setState(obj)
},[obj])
EDIT: Example using useRef
if you don't use the value directly and don't need the component to update based on it:
const obj = {foo: 'bar'}
const [state, setState] = useState(obj)
const { current: payload } = useRef(obj)
useEffect(() => {
// always update the ref with the current value - won't affect renders
payload = obj
// Now manually deep compare here and only update the state if
//needed/you want a re render
if(obj.foo === state.foo) return
setState(obj)
},[obj])
Upvotes: 4
Reputation: 53884
Is there away that I can implement something like shouldComponentUpdate with hooks to tell react to only re-render my component when the data changes?
Commonly, for state change you compare with previous value before rendering with functional useState
or a reference using useRef
:
// functional useState
useEffect(() => {
setInterval(() => {
const curr = { foo: 'bar' };
setPayload(prev => (isEqual(prev, curr) ? prev : curr));
}, 500);
}, [setPayload]);
// with ref
const prev = useRef();
useEffect(() => {
setInterval(() => {
const curr = { foo: 'bar' };
if (!isEqual(prev.current, curr)) {
setPayload(curr);
}
}, 500);
}, [setPayload]);
useEffect(() => {
prev.current = payload;
}, [payload]);
For completeness, "re-render my component when the data changes?" may be referred to props too, so in this case, you should use React.memo
.
If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
Upvotes: 1
Reputation: 39192
The generic solution to this that does not involve adding logic to your effects, is to split your components into:
React.memo
Your dumb component can be pure (as if it had shouldComponentUpdate
implemented and your smart state handling component can be "dumb" and not worry about updating state to the same value.
Example:
export default function Foo() {
const [state, setState] = useState({ foo: "1" })
const handler = useCallback(newValue => setState({ foo: newValue }))
return (
<div>
<SomeWidget onEvent={handler} />
Value: {{ state.foo }}
</div>
)
const FooChild = React.memo(({foo, handler}) => {
return (
<div>
<SomeWidget onEvent={handler} />
Value: {{ state.foo }}
</div>
)
})
export default function Foo() {
const [state, setState] = useState({ foo: "1" })
const handler = useCallback(newValue => setState({ foo: newValue }))
return <FooChild handler={handler} foo={state.foo} />
}
This gives you the separation of logic you are looking for.
Upvotes: 0
Reputation: 69
You can use memoized components, they will re-render only on prop changes.
const comparatorFunc = (prev, next) => {
return prev.foo === next.foo
}
const MemoizedComponent = React.memo(({payload}) => {
return (<div>{JSON.stringify(payload)}</div>)
}, comparatorFunc);
Upvotes: -2