Use Context Api and Immer to Manage the state of your React app
Francisco Mendes
Posted on September 17, 2021
Overview
In the past I've written two articles in which I explain how we can use immer.js together with zustand and the useState hook.
But I am fully aware that not everyone uses community-created state managers or using component state is not enough. That's why many people use the Context Api, it comes with React, it's light, fast and very advanced.
One of the advantages of Context Api is that it is immensely flexible, we can have several implementations of it, but in today's example I will use reducers (which I believe is the most used approach).
In this article I won't explain in depth how immer.js actually works because it's a topic in itself, however I recommend reading this article which explains the concept very well.
Today's example
Today I will take an approach similar to the past, ie at the end of the article I will share with you the github repository so that you can test it more easily.
But talking about the idea now, I'll show you an example of a reducer with a very typical approach, you must have seen it in courses or even at work. Then I'll show you how to get exactly the same results using immer.js.
Let's code
The application that you will have access to the github repository looks like this:
In our application we can add a book, update it and remove it.
Our reducer code is as follows:
// @/src/store/reducers/books.js
export default (state, { payload, type }) => {
switch (type) {
case "ADD_BOOK":
return {
...state,
books: {
...state.books,
list: [...state.books.list, payload],
},
};
case "REMOVE_BOOK":
return {
...state,
books: {
...state.books,
list: state.books.list.filter((book) => book.id !== payload),
},
};
case "UPDATE_BOOK":
return {
...state,
books: {
...state.books,
list: state.books.list.map((book) => {
if (book.id === payload.id) {
return payload;
}
return book;
}),
},
};
default:
return state;
}
};
I believe that practically everyone has seen reducers similar to this one at least once in our lives. I want to say right away that this code isn't incorrect when written this way, it's fully functional, and it's the most popular approach I know.
However this approach is not the most friendly for beginners or people who are not used to working with JavaScript. I say this because at some point, the way we learned to manipulate data structures like objects and arrays is using methods.
And that's exactly why we're going to take into account the logic we have in the code above and we're going to use immer.js now. First let's clean our reducer, like this:
// @/src/store/reducers/books.js
export default (state, { payload, type }) => {
switch (type) {
case "ADD_BOOK":
return;
case "REMOVE_BOOK":
return;
case "UPDATE_BOOK":
return;
default:
return state;
}
};
Let's start working on ADD_BOOK
, in our return we will use the produce()
function from immer.js which will have two arguments. The first argument will be our state and the second will be a callback with our state's draft.
Then to add a new book to our list of books we just need to use the push()
method and we pass the book with a single argument.
// @/src/store/reducers/books.js
import produce from "immer";
export default (state, { payload, type }) => {
switch (type) {
case "ADD_BOOK":
return produce(state, (draft) => {
draft.books.list.push({ ...payload });
});
case "REMOVE_BOOK":
return;
case "UPDATE_BOOK":
return;
default:
return state;
}
};
Now in our REMOVE_BOOK
we will do something similar but this time we will remove a book, first we will need to know the book index with id similar to the payload, using the findIndex()
method. After we get the book's index, we will remove it from the array, using the splice()
method.
// @/src/store/reducers/books.js
import produce from "immer";
export default (state, { payload, type }) => {
switch (type) {
case "ADD_BOOK":
return produce(state, (draft) => {
draft.books.list.push({ ...payload });
});
case "REMOVE_BOOK":
return produce(state, (draft) => {
const bookIndex = draft.books.list.findIndex(
(book) => book.id === payload
);
draft.books.list.splice(bookIndex, 1);
});
case "UPDATE_BOOK":
return;
default:
return state;
}
};
Finally in our UPDATE_BOOK
, we will need to find the book with the id equal to the payload id, using the find()
method. Once we have our book (which this time is an object) let's update each of its properties, like this:
// @/src/store/reducers/books.js
import produce from "immer";
export default (state, { payload, type }) => {
switch (type) {
case "ADD_BOOK":
return produce(state, (draft) => {
draft.books.list.push({ ...payload });
});
case "REMOVE_BOOK":
return produce(state, (draft) => {
const bookIndex = draft.books.list.findIndex(
(book) => book.id === payload
);
draft.books.list.splice(bookIndex, 1);
});
case "UPDATE_BOOK":
return produce(state, (draft) => {
const book = draft.books.list.find((book) => book.id === payload.id);
book.title = payload.title;
book.author = payload.author;
});
default:
return state;
}
};
If you go to test the application you will notice that everything has the same behavior, but this time we have much less code in our reducer, it is easier to read and it is immensely intuitive.
As promised, if you want to access the github repository to test the application click here.
Conclusion
As always, I hope you found it interesting. If you noticed any errors in this article, please mention them in the comments. 🧑🏻💻
Hope you have a great day! 😈
Posted on September 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 20, 2023