This content originally appeared on DEV Community and was authored by Rasaf Ibrahim
In React, useState
and useReducer
are commonly used for managing state. While useImmer
is a popular alternative to useState
, in this article, we will explore how it can also serve as a simpler and more effective alternative of useReducer
.
In case you're not yet familiar with using useImmer
and its usage as an alternative to useState
, you can read the following section. Otherwise, feel free to skip ahead to the main section of this article.
In Case You're Not Familiar with useImmer
useImmer
is a custom React hook. It's similar to useState
but offers some distinct advantages, especially when it comes to managing complex state. With useImmer
, you can update the state as if it were directly mutable, similar to regular JavaScript. It is possible because, behind the scene useImmer
uses the Immer
library to create a new immutable copy of the state.
To use useImmer
, first, you need to install it by running the following command in your terminal:
npm install immer use-immer
A simple example of useImmer
:
/* importing useImmer */
import { useImmer } from 'use-immer';
/* functional component */
function Count() {
/* Declaring a new state variable using the useImmer hook.
Here, we are initializing our state with a single
property called "count" with an initial value of 0 */
const [state, updateState] = useImmer({ count: 0 });
/* Defining a function that will modify our state */
const increment = () => {
/* calling the updateState function and passing as
it a draft */
updateState(draft => {
/* Here we are directly modifying the draft as
if it were mutable */
draft.count += 1;
})
}
/* Defining another function that will modify our state */
const decrement = () => {
/* Calling the updateState function and pass it a draft */
updateState(draft => {
/*Here we are directly modifying the draft as
if it were mutable */
draft.count -= 1;
})
}
/* Rendering the UI using the state */
return (
<div>
<h3>Count: {state.count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
)
}
Why should we use useImmer instead of useReducer
We will explore an example of useImmer
that follows the useReducer
pattern. We will create state and actions and use them in a similar way as useReducer
, but with useImmer's increased readability, flexibility, and mutability. Through this example, we will see why useImmer
should be considered more than the useReducer
.
Import useImmer and Create the Functional Component
// import useImmer
import { useImmer } from 'use-immer';
// functional component
function Cart() {
// ...
}
Define the Initial State
When using useReducer
, it's common practice to define the initial state before proceeding. Similarly, we will use this approach with the useImmer
hook. To do this, we'll create a state that is somewhat complex and resembles the state you would typically define with useReducer
.
// initial state
const initialState = {
items: [],
shippingAddress: {
street: '',
city: ''
}
}
Create the State
We have defined our initial state. Now, we can create our state using the useImmer
hook.
// Creating the state
const [cart, updateCart] = useImmer(initialState)
The useImmer
hook returns an array containing two values: the current state (cart
in this example), and a function that we can use to update the state (updateCart
in this example).
Create Actions
When using the useReducer
hook, it's mandatory to define actions in order to update the state. Similarly, we will create actions for useImmer
to resemble the same pattern and achieve predictable state updates.
const actions = {
// Add an item to the cart
addItemToCart: (payload) => {
const { item } = payload
updateCart(draft => {
draft.items.push(item)
});
},
// Remove an item from the cart
removeItemFromCart: (payload) => {
const { itemIndex } = payload
updateCart(draft => {
draft.items.splice(itemIndex, 1)
})
},
// Update the shipping address of the cart
updateShippingAddress: (payload) => {
const { shippingAddress } = payload
updateCart(draft => {
draft.shippingAddress = shippingAddress
})
}
}
Defining actions using the above method provides two advantages over useReducer
:
⚡️ Mutability: With useImmer
, we have the power of mutable updates which allows for less code and more JavaScript-like code. This is in contrast to useReducer
which requires a more functional programming approach and immutable updates. By using mutable updates in useImmer
, you can achieve the same result with fewer lines of code, making it easier to write and maintain.
⚡️ Readability: Compared to useReducer
, where the actions are typically defined as a switch
case in a reducer function, the useImmer
approach can be more readable as each action is a separate function with a clear and concise name.
Render the UI with JSX
Finally, we can use the cart
state and the actions
object in the JSX to render the UI.
<div>
{/* Displaying the changes of the 'cart' state */}
<p>Cart State: {JSON.stringify(cart)}</p>
{/* Display a list of items in the cart */}
<ul>
{cart.items.map((item, index) => (
<li key={index}>
{item.name} - ${item.price}
{/* Call the removeItemFromCart action when the remove button is clicked */}
<button onClick={() => actions.removeItemFromCart({ itemIndex: index })}>Remove</button>
</li>
))}
</ul>
{/* Call the addItemToCart action when the add item button is clicked */}
<button onClick={() => actions.addItemToCart({ item: { name: 'Product', price: 9.99 } })}>
Add Item
</button>
{/* Allow the user to update the shipping address */}
<div>
<h4>Shipping Address:</h4>
<input type="text" placeholder="Street"
value={cart.shippingAddress.street}
onChange={(e) => actions.updateShippingAddress({ shippingAddress: { ...cart.shippingAddress, street: e.target.value } })}
/>
<input type="text" placeholder="City"
value={cart.shippingAddress.city}
onChange={(e) => actions.updateShippingAddress({ shippingAddress: { ...cart.shippingAddress, city: e.target.value } })}
/>
</div>
</div>
Advantages of Using useImmer over useReducer
We've already explored two advantages(Mutability & Readability) of using useImmer over useReducer in the Create Actions
section. However, there's one more significant advantage worth discussing, which we'll cover in this section.
⚡️ Flexibility: With useImmer, it's possible to update the state object outside of the defined actions. This is not possible in useReducer, and can be particularly helpful in certain situations where you need more flexibility in updating the state.
Here's an example of how to clear the cart using useImmer
without defining a new action:
<button onClick={() => updateCart(initialState)}>Clear Cart</button>
This button component will reset the cart
state back to its initial state and clear all items in the cart. With useImmer
, we can update the state object directly in this ad-hoc manner without the need for an action to be defined. This is not possible with useReducer
, where all state updates must be dispatched through the defined actions.
Full Code
// import useImmer
import { useImmer } from 'use-immer';
// functional component
export default function Cart() {
// Define the initial state of the cart
const initialState = {
items: [],
shippingAddress: {
street: '',
city: ''
}
};
// Call the useImmer hook to create a cart state
const [cart, updateCart] = useImmer(initialState);
// Define a set of actions that can be used to update the cart state
const actions = {
// Add an item to the cart
addItemToCart: (payload) => {
const { item } = payload
updateCart(draft => {
draft.items.push(item)
});
},
// Remove an item from the cart
removeItemFromCart: (payload) => {
const { itemIndex } = payload
updateCart(draft => {
draft.items.splice(itemIndex, 1)
})
},
// Update the shipping address of the cart
updateShippingAddress: (payload) => {
const { shippingAddress } = payload
updateCart(draft => {
draft.shippingAddress = shippingAddress
})
}
}
// Render the cart UI
return (
<div>
{/* Displaying the changes of the 'cart' state */}
<p>Cart State: {JSON.stringify(cart)}</p>
{/* Display a list of items in the cart */}
<ul>
{cart.items.map((item, index) => (
<li key={index}>
{item.name} - ${item.price}
{/* Call the removeItemFromCart action when the remove button is clicked */}
<button onClick={() => actions.removeItemFromCart({ itemIndex: index })}>Remove</button>
</li>
))}
</ul>
{/* Call the addItemToCart action when the add item button is clicked */}
<button onClick={() => actions.addItemToCart({ item: { name: 'Product', price: 9.99 } })}>
Add Item
</button>
{/* Allow the user to update the shipping address */}
<div>
<h4>Shipping Address:</h4>
<input type="text" placeholder="Street"
value={cart.shippingAddress.street}
onChange={(e) => actions.updateShippingAddress({ shippingAddress: { ...cart.shippingAddress, street: e.target.value } })}
/>
<input type="text" placeholder="City"
value={cart.shippingAddress.city}
onChange={(e) => actions.updateShippingAddress({ shippingAddress: { ...cart.shippingAddress, city: e.target.value } })}
/>
</div>
{/* Call the updateCart function with the initial state to clear the cart */}
<button onClick={() => updateCart(initialState)}>Clear Cart</button>
</div>
)
}
That's it. 😃 Thanks for reading. 🎉
This content originally appeared on DEV Community and was authored by Rasaf Ibrahim
Rasaf Ibrahim | Sciencx (2023-02-25T16:41:29+00:00) Ditching useState and useReducer: Why useImmer is the better option. Retrieved from https://www.scien.cx/2023/02/25/ditching-usestate-and-usereducer-why-useimmer-is-the-better-option/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.