useState() Hook is widely used in React applications to re-render the components on state changes. However, there are scenarios where we need to track state changes without re-rendering the components. But, if we use the useRef() Hook, we can track the state changes without causing component re-renderings.
React components automatically re-render whenever there is a change in their state or props. A simple update of the state, from anywhere in the code, causes all the User Interface (UI) elements to be re-rendered automatically. However, there may be cases where the render() method depends on some other data.
As seen in the code examples, using hooks to encapsulate shared logic is a good start, but we can keep exploring more. Inspired by the concept of “partial application”, we can try to return a component with props that have been partially bound from a custom hook.
Turns out, it doesn't really matter, whether the state change in hooks is “internal” or not. Every state change in a hook, whether it affects its return value or not, will cause the “host” component to re-render.
This is possible with useState
or useReducer
, since useState
uses useReducer
internally:
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
forceUpdate
isn't intended to be used under normal circumstances, only in testing or other outstanding cases. This situation may be addressed in a more conventional way.
setCount
is an example of improperly used forceUpdate
, setState
is asynchronous for performance reasons and shouldn't be forced to be synchronous just because state updates weren't performed correctly. If a state relies on previously set state, this should be done with updater function,
If you need to set the state based on the previous state, read about the updater argument below.
<...>
Both state and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with state.
setCount
may not be an illustrative example because its purpose is unclear but this is the case for updater function:
setCount(){
this.setState(({count}) => ({ count: count + 1 }));
this.setState(({count2}) => ({ count2: count + 1 }));
this.setState(({count}) => ({ count2: count + 1 }));
}
This is translated 1:1 to hooks, with the exception that functions that are used as callbacks should better be memoized:
const [state, setState] = useState({ count: 0, count2: 100 });
const setCount = useCallback(() => {
setState(({count}) => ({ count: count + 1 }));
setState(({count2}) => ({ count2: count + 1 }));
setState(({count}) => ({ count2: count + 1 }));
}, []);
Generally, you can use any state handling approach you want to trigger an update.
codesandbox example
const forceUpdate: () => void = React.useState()[1].bind(null, {}) // see NOTE below
const forceUpdate = React.useReducer(() => ({}), {})[1] as () => void
Just wrap whatever approach you prefer like this
function useForceUpdate(): () => void {
return React.useReducer(() => ({}), {})[1] as () => void // <- paste here
}
"To trigger an update" means to tell React engine that some value has changed and that it should rerender your component.
[, setState]
from useState()
requires a parameter. We get rid of it by binding a fresh object {}
.() => ({})
in useReducer
is a dummy reducer that returns a fresh object each time an action is dispatched.{}
(fresh object) is required so that it triggers an update by changing a reference in the state.
PS: useState
just wraps useReducer
internally. source
NOTE: Using .bind with useState causes a change in function reference between renders. It is possible to wrap it inside useCallback as already explained here, but then it wouldn't be a sexy one-liner™. The Reducer version already keeps reference equality between renders. This is important if you want to pass the forceUpdate function in props.
const forceUpdate = React.useState()[1].bind(null, {}) // see NOTE above
const forceUpdate = React.useReducer(() => ({}))[1]
React Hooks FAQ official solution for forceUpdate
:
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
// usage
<button onClick={forceUpdate}>Force update</button>
const App = () => {
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
return (
<div>
<button onClick={forceUpdate}>Force update</button>
<p>Forced update {_} times</p>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.1/umd/react.production.min.js" integrity="sha256-vMEjoeSlzpWvres5mDlxmSKxx6jAmDNY4zCt712YCI0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.1/umd/react-dom.production.min.js" integrity="sha256-QQt6MpTdAD0DiPLhqhzVyPs1flIdstR4/R7x4GqCvZ4=" crossorigin="anonymous"></script>
<script>var useReducer = React.useReducer</script>
<div id="root"></div>
As the others have mentioned, useState
works - here is how mobx-react-lite implements updates - you could do something similar.
Define a new hook, useForceUpdate
-
import { useState, useCallback } from 'react'
export function useForceUpdate() {
const [, setTick] = useState(0);
const update = useCallback(() => {
setTick(tick => tick + 1);
}, [])
return update;
}
and use it in a component -
const forceUpdate = useForceUpdate();
if (...) {
forceUpdate(); // force re-render
}
See https://github.com/mobxjs/mobx-react-lite/blob/master/src/utils.ts and https://github.com/mobxjs/mobx-react-lite/blob/master/src/useObserver.ts
Alternative to @MinhKha's answer:
It can be much cleaner with useReducer
:
const [, forceUpdate] = useReducer(x => x + 1, 0);
Usage:
forceUpdate()
- cleaner without params
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With