One of the core principles of using Redux is to write pure reducers which instead of mutating the state return a new one with the new changes. But as the complexity of your React application grows with time, your redux state also becomes more and more complex. This means that in order to mutate a nested property you will have to use the ES6 spread operator (...
) to create shallow copies of the object at each nesting level and then apply your modifications. Let's see an example :
const initialState={
user:{
cart:{
products:[
{
id:1,
name:'Milk',
price:4,
quantity:1
},
{
id:2,
name:'Butter',
price:2,
quantity:1
},
{
id:3,
name:'Honey',
price:3,
quantity:1
}
]
}
}
}
Here you can see a user's cart state in an e-commerce app. Each product in your products list UI will have a '+' and '-' button with the ability to dispatch a CHANGE_QUANTITY
action which updates the product's quantity. Let's write the reducer handling such action :
const cartReducer=(state=initialState,action)=>{
switch(action.type){
case 'CHANGE_QUANTITY':{
const increment=action.payload.mode=='INCREMENT'?1:-1; // Click on '+' => mode='INCREMENT', Click on '-'=>mode='DECREMENT'
return{
...state,
user:{
...state.user,
cart:{
...state.user.cart,
products:state.cart.products.map((product)=>{
if(product.id===action.payload.productId) // Change the quantity of product where payload productId to update matches the product's id
{
return{
...product,
quantity:product.quantity+increment
}
}
return product;
}).filter((product)=>{
return product.quantity>0;
})
}
}
}
}
}
}
WOAH! That's a lot of code. Let's break it down:
- The user dispatches the action
CHANGE_QUANTITY
using an onClick event handler like this
function changeQuantity(mode,productId) {
dispatch({
type:'CHANGE_QUANTITY',
payload:{
mode, //'INCREMENT/DECREMENT',
productId
}
})
}
In our reducer we construct a new object with shallow copies at each nesting level using
...
operator and map over the products array to modify the quantity for the product withproduct.id===action.payload.productId
.The filter chained to the map is to make sure every time we decrement the quantity of a product with
quantity==1
, the product is removed from the listing.
Our cart functionality works perfectly fine but requires a lot of boilerplate code for creating shallow copies at each level. This is where immer comes to our rescue.
What is Immer?
According to the documentation, immer is a javascript library using which we can:
Create the next immutable state tree by simply modifying the current tree
What does this mean? Let's recreate our cartReducer
using immer to understand
import {produce} from 'immer';
const cartReducer=produce((draft,action)=>{
switch(action.type){
case 'CHANGE_QUANTITY':{
const increment=action.payload.mode=='INCREMENT'?1:-1; // Click on '+' => mode='INCREMENT', Click on '-'=>mode='DECREMENT'
draft.user.cart.products=draft.user.cart.products.map((product)=>{
if(product.id==action.payload.productId) // Change the quantity of product where payload productId to update matches the product's id
{
return{
...product,
quantity:product.quantity+increment
}
}
}).filter((product)=>{
return product.quantity>0;
})
break;
}
}
},initialState);
In immer, we make use of the produce
function to update our state tree. The above code is a curried producer which takes in two arguments - reducer and initialState. Here draft
is the proxy of our current state, which records all the mutations, creates necessary copies without affecting the original state. The produce
applies all these mutations to our original state tree and creates a new state tree. This approach has two main benefits:
- You are able to avoid the boilerplate code of creating shallow copies for each nesting level making the code more readable.
- You can use simple JS functions like
push
,pop
,splice
etc. to mutate the draft object making the code more maintainable.
So in my opinion immer is a great library to use along with Redux when you are handling a complex state tree. BTW, Redux Toolkit created by the authors of Redux, also makes use of immer internally. Another reason to try it out in your next project!