As developers, we often encounter a pivotal question, a fork-in-the-road crossroad in our coding journey. It's the question: "Should I use useState or useReducer for state management in my React applications?" It might seem like a trivial choice to make, but trust me, it is a decision that can greatly influence the maintainability of your code, the efficiency of your application, and not to mention your debugging nightmares.
State management is like the neural network of your application, silently holding the strings behind the scenes and making your app tick. It's crucial and making the right choice can significantly streamline your development process. Both the hooks, useState and useReducer, provided by React bring their unique strengths and trade-offs to the table. Your choice would ultimately depend on the intricacies of your requirements and the specifics of your application's state structure.
In this blog post, my aim is to take you on a journey of exploration, comparing the key features, merits and caveats, of useState and useReducer. I will delve into real-life examples, illustrating their use cases, and even share my own experiences, ups and downs of choosing the best state management hook for various scenarios.
Before diving into the nitty-gritty details, let's start with a solid understanding of useState. As React developers, we are well-acquainted with this simple yet powerful hook. At its core, useState allows us to manage states within functional components.
I adore useState for its straightforward implementation and the ease it brings to handling independent state elements. One of the best parts is that we can have multiple useState hooks within a single component, logically separating different elements of the state. This granular approach ensures clean and maintainable code.
Example: Using Multiple useState Hooks
Let's take a look at an example where we manage two independent state elements: isOpen and counter.
1 import React, { useState } from 'react'; 2 3 function MyComponent() { 4 const [isOpen, setIsOpen] = useState(false); 5 const [counter, setCounter] = useState(0); 6 7 const toggleModal = () => setIsOpen((prevIsOpen) => !prevIsOpen); 8 const incrementCounter = () => setCounter((prevCounter) => prevCounter + 1); 9 10 return ( 11 <div> 12 <button onClick={toggleModal}>Toggle Modal</button> 13 <p>Counter: {counter}</p> 14 {/* Modal and other components */} 15 </div> 16 ); 17 } 18
Single State Object Approach
Sometimes, it makes sense to group related state elements into a single state object. For instance, consider a scenario where we want to manage the coordinates of the mouse pointer.
1 import React, { useState } from 'react'; 2 3 function MouseCoordinates() { 4 const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); 5 6 const handleMouseMove = (event) => { 7 const { clientX, clientY } = event; 8 setMousePosition({ x: clientX, y: clientY }); 9 }; 10 11 return ( 12 <div onMouseMove={handleMouseMove}> 13 <p>Mouse X: {mousePosition.x}</p> 14 <p>Mouse Y: {mousePosition.y}</p> 15 </div> 16 ); 17 } 18
Another area where useState shines is managing form state. We can effortlessly control form inputs and handle their changes without breaking a sweat.
Example: Custom useInput Hook
Let's create a custom useInput hook to manage the form input state.
1 import React, { useState } from 'react'; 2 3 function useInput(initialValue = '') { 4 const [value, setValue] = useState(initialValue); 5 6 const handleChange = (event) => setValue(event.target.value); 7 8 return { 9 value, 10 onChange: handleChange, 11 }; 12 } 13 14 function MyForm() { 15 const firstName = useInput(''); 16 const lastName = useInput(''); 17 18 return ( 19 <form> 20 <input type="text" {...firstName} placeholder="First Name" /> 21 <input type="text" {...lastName} placeholder="Last Name" /> 22 {/* Other form elements */} 23 </form> 24 ); 25 } 26
Now that we've explored useState, it's time to delve into the world of useReducer. A hook might appear intimidating at first, but don't let that deter you. Embrace it, and you'll unlock its full potential for managing complex state and multi-step wizards.
Contrary to popular belief, useReducer is not limited to handling only "complex state." Its flexibility allows us to handle various state scenarios while keeping the application logic neatly organized within the reducer.
Simple useReducer Examples
Let's start with simple examples of how we can use useReducer to toggle state and implement a forceUpdate function.
1 import React, { useReducer } from 'react'; 2 3 // Toggle state reducer 4 const toggleReducer = (state, action) => !state; 5 6 function Toggle() { 7 const [isOn, dispatch] = useReducer(toggleReducer, false); 8 9 return ( 10 <div> 11 <button onClick={dispatch}>Toggle</button> 12 {isOn ? <p>ON</p> : <p>OFF</p>} 13 </div> 14 ); 15 } 16 17 // ForceUpdate reducer 18 const forceUpdateReducer = (state) => state + 1; 19 20 function ForceUpdate() { 21 const [, forceUpdate] = useReducer(forceUpdateReducer, 0); 22 23 return ( 24 <div> 25 <button onClick={forceUpdate}>Force Update</button> 26 {/* Other components */} 27 </div> 28 ); 29 } 30
One of the keys to using useReducer effectively is to model actions as events. Each action describes an event that changes the state. This way, we maintain a clear and structured way of managing state transitions.
Passing Props and Server State into Reducers
Intriguingly, we can pass props and even server state into our reducers. By doing so, we empower useReducer with additional contextual information, making it even more powerful.
During my adventures with useReducer, I stumbled upon an issue that developers commonly face with useState - the infamous stale closure bug. Allow me to share my story and its resolution.
The useUndo Example
Imagine we want to create a custom useUndo hook that enables undo and redo functionality for a specific value.
1 2 function Example() { 3 const [state, { set }] = useUndo('first'); 4 5 React.useEffect(() => { 6 set('second'); 7 }, []); 8 9 React.useEffect(() => { 10 set('third'); 11 }, []); 12 } 13
The Infinite Loop
To my dismay, the useUndo hook suffered from an infinite loop, thanks to the stale closure bug. The issue arose when calling set inside the useEffect without including it in the dependency array.
Solution with useReducer
After some head-scratching, I discovered that useReducer could rescue us from this predicament. By restructuring the hook and utilizing state updater callbacks, I could eliminate the need to include a set in the dependency array.
1 2 function useUndo(initialPresent) { 3 const [state, setState] = React.useReducer(undoReducer, { 4 past: [], 5 present: initialPresent, 6 future: [], 7 }); 8 9 // Other parts of the hook 10 } 11
The transition to useReducer revealed the beauty of keeping logic within the reducer itself. No more worrying about stale closures or dependency arrays! The reducer acts as the single source of truth, managing state transitions and keeping our code clean and efficient.
Now that we have a firm grasp on both useState and useReducer, let's compare the two hooks and understand when to use each of them in real-world scenarios.
useState is the go-to hook when dealing with independent state elements. It shines when we want to manage multiple state fields that are not interdependent. Its simplicity and ability to handle separate state elements make it an excellent choice for many situations.
On the other hand, useReducer prevails when state elements rely on one another for updates. When a change in one state affects the transition of another state, useReducer steps up to the plate, ensuring smooth and predictable state management.
Remember, it's not about a strict rule of choosing one over the other. You can use both hooks in harmony within a single component or hook. The key is to logically separate state elements based on their dependencies and requirements.
As we reach the end of our state management journey, I hope you've enjoyed exploring the nuances of useState and useReducer. Both hooks bring valuable tools to the table, and your choice depends on the context of your application.
In my experience, I suggest starting with useState, and as your state management needs to evolve, consider transitioning to useReducer when interdependent state elements become prevalent.
The conclusion of this blog seems like the perfect opportunity to introduce a powerful plugin that can integrate seamlessly with your React project — WiseGPT. This plugin, developed by DhiWise, makes generating code for APIs straightforward.
WiseGPT is exceptional in that it mirrors your coding style, eliminates any hard limit on the output size, and makes it easier by eliminating the need for prompts. The plugin auto-creates models and functions, which takes the manual work out of API requests, response parsing, error management strategies, and it wonderfully handles intricate API endpoints.
I encourage you to try WiseGPT and see how it integrates seamlessly into your React projects. Streamline your coding process and focus your effort on implementing the best state management strategy for your app, be it useState, useReducer, or a combination of both.
To all React developers out there, let's appreciate the power of these incredible hooks and the automation capability that tools like WiseGPT can introduce into our code.
Happy coding! 😊
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.