We know that JavaScript is a dynamically, multi-paradigm, weakly typed language. This means that we can apply a lot of different paradigms in our JavaScript code, we can have, for example, object-oriented JavaScript, imperative JavaScript, functional programming JavaScript, etc. A lot of JavaScript developers started to adopt the functional programming paradigm in their applications.
A few libraries were created, and now that the adoption of React is expanding and growing massively among developers, the immutability concept is starting to be used and discussed more often as well. Let’s first understand what immutability is and then we’ll look at how we can use this concept of functional programming in our React applications.
In a functional programming language, one of the most interesting and important concepts is immutability. The whole meaning of immutability is “unable to change”, if we have an object and want to create a new object, we should copy the actual object and not mutate it.
When creating applications we need to think about the user and, more importantly, the user data. All the data that’s created, modified, deleted, and replaced in your application is important and should be watched, stored, and managed properly. That’s why we should create better standards or concepts to deal with our data.
But why should we have an immutable code in our application? Well, we can use immutability to benefit in some points, for example:
In React applications, the most important parts of your application is the state data. You should care and manage it properly, otherwise, it will cause bugs and you will lose data very easily, which can be your worst nightmare.
It’s well known by React developers that we should not mutate our state directly, but use the setState
method. But why?
This is one of the main ideas behind React — track changes and if something changes, rerender the component. You cannot simply change your state mutably, because it’ll not trigger a rerender in your component. By using the setState
method, you will create a new state in an immutable way, React will know that something changed, and will rerender the respective component.
We also have similar behavior in Redux, the most famous and used state management library for React applications. Redux represents the state as immutable objects, to change your state you should pass your new state data using pure functions, these pure functions are called reducers
. Reducers should never mutate the state, to avoid side effects in your application, and make sure that Redux keeps track of the current state data.
We can see that the concept of immutability is getting used more and becoming more common in the React community. But to make sure that we’re doing it the right way, we can use a library for the job.
To better deal with state data, a library was created to help us, called Immer. Immer was created to help us to have an immutable state, it’s a library created based on the “copy-on-write” mechanism — a technique used to implement a copy operation in on modifiable resources.
Immer is very easy to understand, this is how Immer works:
To start using Immer, you need first to install it:
yarn add immer
Now we’re going to import Immer inside our component. The library exports a default function called produce
:
produce(currentState, producer: (draftState) => void): nextState
The first argument of the produce
function is our current state object, the second argument is a function, which will get our draft
state and then perform the changes that we want to.
Let’s create a simple component called Users
and we’ll make a list of users. We’ll create a simple state called users
, which will be an array of users, and another state called users
which will be an object. Inside that object, we’ll have the name
of the user
:
this.state = { user: { name: "", }, users: [] }
Now, let’s import the produce
function from Immer and create a new function called onInputChange
. Every time we type on the input, we’ll change the value of the name
of the user
.
onInputChange = event => { this.setState(produce(this.state.user, draftState => { draftState.user = { name: event.target.value } })) }
The setState
method from React accepts a function, so we’re passing the produce
function from Immer, inside the produce
function we’re passing as a first argument our user
state, and as a second argument, we’re using a function. Inside that function, we’re changing our draftState
of user
to be equal to the input value. So, we’re tracking the value of the input and saving it on our user
state.
Now that we’re saving our user state correctly, let’s submit a new user every time we click on the button. We’ll create a new function called onSubmitUser
, and our function is going to look like this:
onSubmitUser = () => { this.setState(produce(draftState => { draftState.users.push(this.state.user); draftState.user = { name: "" } })) }
You can notice now that we’re using the setState
again, passing our produce
function, but now we’re only using the draftState
as an argument, and we are no longer using the current state as an argument. But why?
Well, Immer has something called curried producers, if you pass a function as the first argument to your produce
function, it’ll be used for currying. We have a “curried” function now, which means that this function will accept a state, and call our updated draft function.
So, in the end, our whole component will look like this:
class Users extends Component { constructor(props) { super(props); this.state = { user: { name: "" }, users: [] }; } onInputChange = event => { this.setState( produce(this.state.user, draftState => { draftState.user = { name: event.target.value }; }) ); }; onSubmitUser = () => { this.setState( produce(draftState => { draftState.users.push(this.state.user); draftState.user = { name: "" }; }) ); }; render() { const { users, user } = this.state; return ( <div> <h1>Immer with React</h1> {users.map(user => ( <h4>{user.name}</h4> ))} <input type="text" value={user.name} onChange={this.onInputChange} /> <button onClick={this.onSubmitUser}>Submit</button> </div> ); } }
Now that we created our example using Immer with class components, you might be asking is it possible to use Immer with React Hooks? Yes, it is!
The useImmer
Hook is pretty similar to the useState
Hook from React. First, let’s install it:
yarn add use-immer
Let’s create a new component called UserImmer
, inside that component we’re going to import the useImmer
Hook from use-immer
:
import React from 'react'; import { useImmer } from "use-immer"; const UserImmer = () => { ... } export default UserImmer;
We’re going to have two states in our component. We’ll have users
for our list of users, and user
:
const [user, setUser] = useImmer({ name: '' }) const [users, setUsers] = useImmer([])
Now, let’s create a function with the same name as the previous example, onInputChange
, and inside that function, we’re going to update the value of our user
:
const onInputChange = (user) => { setUser(draftState => { draftState.name = user }) }
Let’s now create our onSubmitUser
function, which will add a new user every time we click on the button. Pretty similar to the previous example:
const onSubmitUser = () => { setUsers(draftState => { draftState.push(user) }) setUser(draftState => { draftState.name = "" }) }
You can see that we’re using both setUsers
and setUser
function. We’re using the setUsers
function first to add the user
to our users
array. After that, we’re using the setUser
function just to reset the value of the name
of the user
to an empty string.
Our whole component will look like this:
import React from 'react'; import { useImmer } from "use-immer"; const UserImmer = () => { const [user, setUser] = useImmer({ name: '' }) const [users, setUsers] = useImmer([]) const onInputChange = (user: any) => { setUser(draftState => { draftState.name = user }) } const onSubmitUser = () => { setUsers(draftState => { draftState.push(user) }) setUser(draftState => { draftState.name = "" }) } return ( <div> <h1>Users</h1> {users.map((user, index) => ( <h5 key={index}>{user.name}</h5> ))} <input type="text" onChange={e => onInputChange(e.target.value)} value={user.name} /> <button onClick={onSubmitUser}>Submit</button> </div> ) } export default UserImmer;
We have now a component using Immer with an immutable state. This is very easy to start, easier to maintain, and our code gets a lot more readable. If you’re planning to start with immutability in React and want to make your state immutable and safer, Immer is your best option.
Another thing that might be important for you to know is you can use Immer not only with React but with plain JavaScript as well. So, if you’re going to build a simple application using vanilla JavaScript, and you want to have an immutable state, you can use Immer very easily. In the long-term, it will help you a lot to have a more confident, well-written, and maintainable application.
In this article, we learned about immutability in React and how we can use Immer to have an immutable state — making our application safer, readable, and maintainable. To learn more about Immer, you can check out its documentation, and if you want to learn more about this fantastic library, you can take this course.
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>
Would you be interested in joining LogRocket's developer community?
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 nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.