Leonardo Maldonado Full Stack Developer. JavaScript, React, TypeScript, GraphQL.

Immutability in React with Immer

6 min read 1751

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.

Immutability

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:

  • Readability —  if you have an immutable code, your application will get more readable for you and your team, it’ll get easier to understand exactly what’s happening and what every piece of code is performing
  • Maintainability —  with an immutable code, your application will get a lot easier to debug and maintain, when errors happen — and this is something we cannot avoid easily, they always happen — it will get very easy to find where it happened and where things went wrong
  • Fewer side effects  —  this could be a more positive point for maintainability, but when you have an immutable code, the chances to have negative side effects in your application decreases. You will end up with more manageable code, and the chances to have unexpectable errors in your applications decreases

Immutability in React

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.

We made a custom demo for .
No really. Click here to check it out.

Immer

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:

  1. You have your actual state data
  2. Immer will copy your actual state data, and create a new temporary “draft” of it. This draft will be a proxy of the next state data
  3. After the draft is created, Immer will update your state data with the draft, which is a proxy of your next state data
  4. To simplify, this is how Immer will deal with your state:

how Immer deals with state

Getting started

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!

useImmer Hook

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 setUsersfunction 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.

Conclusion

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.

 

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Leonardo Maldonado Full Stack Developer. JavaScript, React, TypeScript, GraphQL.

Leave a Reply