DEV Community

Er Hardik Chauhan
Er Hardik Chauhan

Posted on

Immer : Clear understanding of how to handle nested state objects with Immer in React and TypeScript

A Step-by-Step Guide to Using Immer in TypeScript and React

Introduction

In modern React development, managing state immutably is crucial for predictable behavior and performance optimizations. However, ensuring immutability can be challenging, especially when dealing with complex or deeply nested state objects. This is where Immer comes in. Immer allows you to work with immutable state in a more convenient and readable way by using a concept called "draft state."

In this blog post, we'll walk through how to integrate Immer into a React project using TypeScript. We'll cover the basics of Immer, setting up a React project with TypeScript, and using Immer to manage state immutably, including handling deeply nested state objects.

Prerequisites

  • Basic knowledge of React and TypeScript.
  • Node.js and npm/yarn installed on your machine.

Step 1: Setting Up the React Project

First, let's set up a new React project with TypeScript.

npx create-react-app my-immer-app --template typescript
cd my-immer-app
Enter fullscreen mode Exit fullscreen mode

Once the project is set up, you can start the development server to ensure everything is working correctly.

npm start
Enter fullscreen mode Exit fullscreen mode

This should start a development server on http://localhost:3000.

Step 2: Installing Immer

Immer is not included by default in React projects, so you need to install it:

npm install immer
Enter fullscreen mode Exit fullscreen mode

After installing Immer, you’re ready to start using it in your project.

Step 3: Understanding the Basics of Immer

Immer simplifies immutable updates by using the concept of a "draft state." Instead of directly modifying the state, you modify the draft state. Once the modifications are done, Immer produces the next immutable state based on the changes made to the draft.

Here's a basic example:

import produce from 'immer';

const state = {
  name: 'Alice',
  age: 25,
};

const nextState = produce(state, (draft) => {
  draft.age = 26;
});

console.log(state); // { name: 'Alice', age: 25 }
console.log(nextState); // { name: 'Alice', age: 26 }
Enter fullscreen mode Exit fullscreen mode

In this example, the original state remains unchanged, while nextState reflects the changes made during the produce function.

Step 4: Using Immer in a React Component

Now, let's integrate Immer into a React component. We’ll create a simple counter component that uses Immer to manage state immutably.

import React, { useState } from 'react';
import produce from 'immer';

const Counter: React.FC = () => {
  const [state, setState] = useState({ count: 0 });

  const increment = () => {
    setState((currentState) =>
      produce(currentState, (draft) => {
        draft.count += 1;
      })
    );
  };

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;
Enter fullscreen mode Exit fullscreen mode

In this component:

  1. We initialize the state with useState.
  2. The increment function updates the count by using Immer's produce function.
  3. The setState function is called with the updated state, ensuring that the state remains immutable.

Step 5: Handling Nested State

Immer is particularly useful when dealing with deeply nested state objects. Consider a state structure like this:

interface Address {
  city: string;
  zip: string;
}

interface UserProfile {
  name: string;
  age: number;
  address: Address;
}

const initialState: UserProfile = {
  name: 'Alice',
  age: 25,
  address: {
    city: 'New York',
    zip: '10001',
  },
};
Enter fullscreen mode Exit fullscreen mode

If you want to update the zip code in the address object, Immer makes it easy:

import React, { useState } from 'react';
import produce from 'immer';

const UserProfileComponent: React.FC = () => {
  const [state, setState] = useState(initialState);

  const updateZip = (newZip: string) => {
    setState((currentState) =>
      produce(currentState, (draft) => {
        draft.address.zip = newZip;
      })
    );
  };

  return (
    <div>
      <p>Name: {state.name}</p>
      <p>Age: {state.age}</p>
      <p>City: {state.address.city}</p>
      <p>Zip: {state.address.zip}</p>
      <button onClick={() => updateZip('10002')}>Update Zip</button>
    </div>
  );
};

export default UserProfileComponent;
Enter fullscreen mode Exit fullscreen mode

Here:

  • We use produce to update the nested address.zip property.
  • The original state remains untouched, and a new state object is created with the updated values.

Step 6: More Complex Nested State Example

Let’s explore an even more complex example where you need to update multiple nested properties. Consider a more detailed UserProfile:

interface Contact {
  email: string;
  phone: string;
}

interface Address {
  city: string;
  zip: string;
}

interface UserProfile {
  name: string;
  age: number;
  address: Address;
  contact: Contact;
}

const initialState: UserProfile = {
  name: 'Alice',
  age: 25,
  address: {
    city: 'New York',
    zip: '10001',
  },
  contact: {
    email: 'alice@example.com',
    phone: '123-456-7890',
  },
};
Enter fullscreen mode Exit fullscreen mode

Suppose you want to update both the city and the phone in one operation:

import React, { useState } from 'react';
import produce from 'immer';

const UserProfileComponent: React.FC = () => {
  const [state, setState] = useState(initialState);

  const updateUserInfo = (newCity: string, newPhone: string) => {
    setState((currentState) =>
      produce(currentState, (draft) => {
        draft.address.city = newCity;
        draft.contact.phone = newPhone;
      })
    );
  };

  return (
    <div>
      <p>Name: {state.name}</p>
      <p>Age: {state.age}</p>
      <p>City: {state.address.city}</p>
      <p>Zip: {state.address.zip}</p>
      <p>Email: {state.contact.email}</p>
      <p>Phone: {state.contact.phone}</p>
      <button onClick={() => updateUserInfo('Los Angeles', '987-654-3210')}>
        Update City and Phone
      </button>
    </div>
  );
};

export default UserProfileComponent;
Enter fullscreen mode Exit fullscreen mode

In this example:

  • We update the city in the address and the phone in the contact objects simultaneously.
  • Immer handles these complex nested updates smoothly, ensuring the state remains immutable.

Step 7: Conclusion

Immer is a powerful tool that simplifies working with immutable state in React applications, especially when using TypeScript. It provides a more readable and less error-prone way to manage state updates, making your code cleaner and easier to maintain.

By integrating Immer into your React projects, you can ensure that your state management remains predictable and robust, even as your application grows in complexity.

This guide has shown you how to handle both simple and complex state updates, including deeply nested state objects. Immer helps maintain immutability in a straightforward way, which is essential for building reliable and maintainable React applications.

Additional Resources

I hope this guide gives you a strong foundation for using Immer with React and TypeScript in your projects. Happy coding!


This version of the blog should give you a clear understanding of how to handle nested state objects with Immer in React and TypeScript.

Top comments (0)