Editor’s note: This article was last updated on 13 February 2023 to ensure that content is up to date and to include a section on resetting state using the React key
property. For more articles on React Hooks check out this cheat sheet.
With the advent of Hooks, the preferred way to share logic between components is via reusable custom Hooks. To create truly reusable custom Hooks, you should adopt the tried-and-tested advanced React component patterns. One of these patterns is called the state initializer pattern.
In this article, we will answer the question of what is the state initializer pattern and how does it work? We’ll also explore why is it important, and more importantly, how the pattern is implemented with Hooks.
If you’re curious about implementing every advanced React pattern with Hooks, you should get my book, “Reintroducing React.” I discuss the subject with care and detail.
Please note that the following sections of the article assume basic fluency in Hooks.
Jump ahead:
useState
lazy initializationGenerally speaking, “to initialize” means to set the value of something. Going by this definition, initializing state in the context of a React application means setting the app’s initial state.
The state initializer pattern is a React pattern that allows for setting the initial state of a custom hook and also resetting it to the initial state.
This pattern is important because it makes it possible for the consumers of your custom hook to decide what the initial state of the hook should be, and to reset the state to the initial state whenever they want. In addition, the state initializer pattern allows the consumer to perform any side effects after resetting the state.
To implement the state initializer pattern with hooks, you need to fulfill three requirements:
initialState
to the useState
hook within your custom hookreset
function from your custom hook that resets the state to the initial stateonReset
as an argument to your custom hook. The onReset
function will be called after the state has been reset to the initial stateHere’s an example implementation of the state initializer pattern with hooks:
function useCounter(initialCount = 0, onReset) { const [count, setCount] = useState(initialCount); const reset = useCallback(() => { setCount(initialCount); onReset && onReset(); }, [initialCount, onReset]); return { count, setCount, reset }; }
In the example above, the useCounter
hook takes an optional initialCount
argument, which defaults to 0 if not provided. It also takes an optional onReset
function that will be called after the state has been reset.
The useCallback
hook is used to memoize the reset
function to prevent unnecessary re-renders. The reset
function sets the count to the initialCount
and calls the onReset
function if it exists.
With this implementation, consumers of the useCounter
hook can now configure the initial state, reset the state, and perform any side effects after resetting the state.
I am going to discuss this subject pragmatically, so here’s the demo app we will work with.
It’s a little contrived, but I promise it takes nothing away from actually understanding the state initializer pattern with Hooks:
What we have here is a glorified counter application. You click the More coffee button and the number of coffee cups increases.
The main App
component utilizes a custom hook to manage the number of coffee cups. Here’s what the implementation of the custom hook, useCounter
, looks like:
// the App uses this custom hook to track the count of coffee cups const useCounter = () => { const [count, setCount] = useState(1); return { count, setCount }; }
A more prudent implementation of the custom hook above would be memoizing the returned object value from the custom hook:
// good return { count, setCount }; // better return useCallback(() => { count, setCount })
Let’s move on.
To be sure you’re still on track, here are the requirements fulfilled by the state initializer pattern:
The first requirement of the pattern happens to be the easiest to resolve. Consider the initial implementation of the custom Hook:
function useCounter () { const [count, setCount] = useState(1); return { count, setCount }; }
On line 2, the initial state within the Hook is set:
const [count, setCount] = useState(1)
Instead of hardcoding the initial state, edit the Hook to expect an argument called initialCount
and pass this value to the useState
call:
function useCounter (initialCount) { const [count, setCount] = useState(initialCount); return { count, setCount }; }
To be slightly more defensive, set a fallback via the default parameter syntax. This will cater to users who don’t pass this initialCount
argument:
function useCounter (initialCount = 1) { const [count, setCount] = useState(initialCount); return { count, setCount }; }
Now the custom Hook should work as before, but with more flexibility on initializing state. I’ll go ahead and initialize the number of initial coffees cups to 10, as seen below:
This is exactly how a consumer would initialize state with the implemented functionality. Let’s move on to fulfilling the other requirements.
To handle resets, we need to expose a callback the consumer can invoke at any point in time. Here’s how. First, create a function that performs the actual reset within the custom Hook:
function useCounter (initialCount = 1) { const [count, setCount] = useState(initialCount); // look here 👇 const reset = useCallback(() => { setCount(initialCount) }, [initialCount]) return { count, setCount }; }
We optimize the reset callback by utilizing the useCallback
Hook. Note that within the reset callback is a simple invocation of the state updater, setCount
:
setCount(initialCount)
This is responsible for setting the state to the initial value passed in by the user, or the default you’ve provided via the default parameter syntax. Now, expose this reset callback in the returned object value, as shown below:
... return { count, setCount, reset }
Now, any consumer of this custom Hook may retrieve the reset callback and perform a reset whenever they want. Below is an example:
key
propertyDigressing a little bit from our demo application, another way to reset state in React is by using the key
property. By changing the value of the key
property of a component, React will treat it as a new component and unmount the old one, effectively resetting its state.
This technique can be useful in cases where you want to reset a component’s state without actually changing its parent component’s state, or when you want to completely remove and re-add a component to the DOM.
To use this technique, simply pass a new value to the key
prop of the component that you want to reset. For example:
function MyComponent() { const [count, setCount] = useState(0); function reset() { setCount(0); } return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> <button onClick={reset}>Reset</button> </div> ); } function App() { const [resetKey, setResetKey] = useState(0); function handleReset() { setResetKey(resetKey + 1); } return ( <div> <MyComponent key={resetKey} /> <button onClick={handleReset}>Reset MyComponent</button> </div> ); }
In this example, we’re passing a new value to the key
prop of MyComponent
every time we want to reset it, by incrementing the resetKey
state variable. This causes React to unmount the old MyComponent
instance and create a new one with a fresh state.
Note that this technique should be used with caution, as it can lead to unnecessary re-renders and performance issues if not used properly. It’s generally recommended to use the state initializer pattern or other techniques like Redux for managing complex states in your React applications.
Finally, we’re on the last requirement of the state initializer pattern. Do you have an idea about how this may be done (i.e., trigger a side effect)? It’s a little tricky, but very easy to handle.
First, consider how side effects are triggered in a typical functional component:
useEffect(() => { // perform side effect here }, [dependency])
We can safely assume that the consumer of this component will do something similar. What is there to be exposed from the custom Hook to make this possible?
Well, look at the value passed to the useEffect
array dependency. You need to expose a dependency — one that only changes when a reset has been triggered internally, i.e., after the consumer invokes the reset callback.
There are two different ways to approach this. I took the liberty of explaining both in “Reintroducing React.”
Here’s what I consider the preferred solution:
function useCounter(initialCount = 1) { const [count, setCount] = useState(initialCount); // 1. look here 👇 const resetRef = useRef(0); const reset = useCallback(() => { setCount(initialCount); // 2. 👇 update reset count ++resetRef.current; }, [initialCount]); return { count, setCount, reset, resetDep: resetRef.current // 3. 👈 expose this dependency }; }
If you look in the code above, you’ll find three annotated lines.
First, create a ref
to hold the number of resets that have been triggered. This is done via the useRef
Hook:
... // 1. look here 👇 const resetRef = useRef(0); ...
Whenever the reset callback is invoked by the user, you need to update the reset ref count:
... const reset = useCallback(() => { setCount(initialCount); // 2. 👇 update reset count ++resetRef.current; }, [initialCount]); ...
Finally, expose this reset count as resetDep
, reset dependency:
... return { count, setCount, reset, resetDep: resetRef.current // 3. 👈 expose this dependency }; ...
The user may then retrieve this reset dependency, resetDep
, and perform a side effect only when this value changes.
This begs the question, how will the consumer use this exposed resetDep
? I’ll go a bit further to explain how this reset dependency would be consumed by the consumer of your custom Hook.
Quick teaser: do you think the solution below would work?
// consumer's app const { resetDep } = useCounter() useEffect(() => { // side effect after reset }, [resetDep])
Unfortunately, that’s not going to work as intended. So, what’s wrong with the solution above?
The problem here is that useEffect
is always first triggered when the component first mounts! Consequently, the reset side effect will be triggered on mount and, subsequently, whenever the resetDep
changes.
This isn’t the behavior we seek; we don’t want the reset side effect triggered on mount. To fix this, the user may provide a check for when the component just mounts, and only trigger the effect function afterward.
Here’s a solution:
// consumer's app const {resetDep} = useCounter() // boolean ref. default to true const componentJustMounted = useRef(true) useEffect(() => { if(!componentJustMounted) { // perform side effect //only when the component isn't just mounted } // if not set boolean ref to false. componentJustMounted.current = false; }, [resetDep])
However, if you’ve created a popular reusable Hook or just want to expose an easier API for the consumers of the Hook, then you may wrap and expose all the functionality above in another custom Hook to be used by the consumer — something like useEffectAfterMount
.
Regardless, the implementation of the reset dependency stands. No changes need to be made internally.
useState
lazy initializationLazy initialization is a technique that can be used to improve the performance of your application. It involves delaying the initialization of a value until it is actually needed.
In the context of React and the useState
Hook, lazy initialization can be used with the state initializer pattern to optimize the rendering of your components.
The state initializer pattern involves passing a function as the initial state value to the useState
Hook. This function will be called only once when the component is mounted, and its return value will be used as the initial state. By using a function, you can compute the initial state value dynamically, which can be useful in some cases.
With lazy initialization, you can delay the execution of this function until it is actually needed. For example, if the initial state value is only needed after some user interaction, you can delay the computation until that interaction occurs. This can reduce the amount of work that needs to be done during the initial rendering of the component, which can improve the perceived performance of your application.
Here’s an example of using lazy initialization with the state initializer pattern:
function MyComponent() { const [count, setCount] = useState(() => { // This function will be called only once, when the component is mounted // The expensive computation is delayed until it is actually needed return computeInitialCount(); }); function handleClick() { setCount(count + 1); } return ( <div> <p>Count: {count}</p> <button onClick={handleClick}>Increment</button> </div> ); }
In this example, the expensive computation to compute the initial count value is delayed until it is actually needed. This can improve the perceived performance of the component, especially if the computation is complex or involves fetching data from an external source.
However, it’s important to use lazy initialization judiciously, as it can also lead to unexpected behavior if not used correctly. Be sure to consider the trade-offs between lazy initialization and eager initialization, and choose the approach that is best for your specific use case.
Design patterns exist to provide consistent solutions to common problems. Advanced React design patterns also exist for providing consistent solutions to building truly reusable components.
Want to learn more about building truly reusable Hooks? Check out my book, “Reintroducing React.”
Catch you later!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowEfficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
4 Replies to "How to initialize state using React Hooks"
great post. but what problem does this design pattern solve ?
You’d typically use this pattern in conjunction with more interesting patterns such as control props/state reducer etc. Regardless, this pattern exists to make it easier to initialise and reset state within your reusable components.
For me in the useCounter I have to return array instead of object
return [count, setCount];
else wil get the Invalid attempt to destructure non-iterable instance error
Your custom hook will return an invalid function or return value is not iterable error because you return an object and not an array.
you should do this.
return [count, setCount, reset];