Using Immer for React State Management

We make use of state to keep track of application data. States change as users interact with an application. When this happens, we need to update the state that is displayed to the user, and we do this using React’s setState.

Since states are not meant to be updated directly (because React’s state has to be immutable), things can get really complicated as states become more complex. They become difficult to understand and follow.

This is where Immer comes in and that’s what we’re going to look at in this post. Using Immer, states can be simplified and much easier to follow. Immer makes use of something called "draft" which you can think of as the copy of your state, but not the state itself. It’s as though Immer hit CMD+C on the state and then cmd+V’d it somewhere else where it can be safely viewed without disturbing the original copy. Any updates you need to make happen on the draft, and the parts of the current state that change on the draft is updated.

Let’s say your application’s state looks like this;

this.state = {
  name: 'Kunle',
  age: 30,
  city: 'Lagos,
  country: 'Nigeria'
}

This user happens to be celebrating his 31st birthday and which means we need to update the age value. With Immer running behind the scenes, a replica of this state will be made.

Now imagine the replica is made and handed over to a messenger, who gives the newly copied version of the state to Kunle. It means there are now two copies available — the current state and the draft copy that was handed over. Kunle then changes the age on the draft to 31. The messenger then returns to the application with the draft, compares both versions, and only updates the age since that’s the only part of the draft that changed.

It does not break the idea of an immutable state, as the current state does not get updated directly. Immer basically makes it convenient to work with immutable state.

Let’s look at an example of this at work

Say you want to build a traffic light for your community, you can give it a shot using Immer for your state updates.

See the Pen
Traffic Light Example with Reactjs
by CarterTsai (@CarterTsai)
on CodePen.

Using Immer, the component will look like this:

const {produce} = immer

class App extends React.Component {
  state = {
    red: 'red', 
    yellow: 'black', 
    green: 'black',
    next: "yellow"
  }

  componentDidMount() {
    this.interval = setInterval(() => this.changeHandle(), 3000);
  }
  
  componentWillUnmount()  {
    clearInterval(this.interval);
  }

  handleRedLight = () => {
    this.setState(
      produce(draft => {
        draft.red = 'red';
        draft.yellow = 'black';
        draft.green = 'black';
        draft.next = 'yellow'
      })
    )
  }
  
  handleYellowLight = () => {
    this.setState(
      produce(draft => {
        draft.red = 'black';
        draft.yellow = 'yellow';
        draft.green = 'black';
        draft.next = 'green'
      })
    )
  }
  
  handleGreenLight = () => {
    this.setState(
      produce(draft => {
        draft.red = 'black';
        draft.yellow = 'black';
        draft.green = 'green';
        draft.next = 'red'
      })
    )
  }

  changeHandle = () => {
    if (this.state.next === 'yellow') {
      this.handleYellowLight()
    } else if (this.state.next === 'green') {
      this.handleGreenLight()
    } else {
      this.handleRedLight()
    }
    
  }

  render() {
    return (
      <div className="box">
        <div className="circle" style={{backgroundColor: this.state.red}}></div>
        <div className="circle" style={{backgroundColor: this.state.yellow}}></div>
        <div className="circle" style={{backgroundColor: this.state.green}}></div>
      </div>
  );
}
};

produce is the default function we get from Immer. Here, we pass it as a value to the setState() method. The produce function takes a function which accepts draft as an argument. It is inside this function that we can then set the draft copy with which we want to update our state.

If that looks complicated, there is another way to write this. First, we create a function.

const handleLight = (state) => {
  return produce(state, (draft) => {
    draft.red = 'black';
    draft.yellow = 'black';
    draft.green = 'green';
    draft.next = 'red'
  });
}

We are passing the current state of the application, and the function which accepts draft as arguments to the produce function. To make use of this inside our component, we do this;

handleGreenLight = () => {
  const nextState = handleLight(this.state)
  this.setState(nextState)
}

Another example: A shopping list

If you have been working with React for a while now, then you’re not a stranger to the spread operator. With Immer, you need not make use of the spread operator, especially when working with an array in your state.

Let’s explore that a little further by creating a shopping list application.

See the Pen
immer 2 - shopping list
by Kingsley Silas Chijioke (@kinsomicrote)
on CodePen.

Here’s the component we’re working with:

class App extends React.Component {
  constructor(props) {
      super(props)
      
      this.state = {
        item: "",
        price: 0,
        list: [
          { id: 1, name: "Cereals", price: 12 },
          { id: 2, name: "Rice", price: 10 }
        ]
      }
    }

    handleInputChange = e => {
      this.setState(
      produce(draft => {
        draft[event.target.name] = event.target.value
      }))
    }

    handleSubmit = (e) => {
      e.preventDefault()
      const newItem = {
        id: uuid.v4(),
        name: this.state.name,
        price: this.state.price
      }
      this.setState(
        produce(draft => {
          draft.list = draft.list.concat(newItem)
        })
      )
    };

  render() {
    return (
      <React.Fragment>
        <section className="section">
          <div className="box">
            <form onSubmit={this.handleSubmit}>
              <h2>Create your shopping list</h2>
              <div>
                <input
                  type="text"
                  placeholder="Item's Name"
                  onChange={this.handleInputChange}
                  name="name"
                  className="input"
                  />
              </div>
              <div>
                <input
                  type="number"
                  placeholder="Item's Price"
                  onChange={this.handleInputChange}
                  name="price"
                  className="input"
                  />
              </div>
              <button className="button is-grey">Submit</button>
            </form>
          </div>
          
          <div className="box">
            {
              this.state.list.length ? (
                this.state.list.map(item => (
                  <ul>
                    <li key={item.id}>
                      <p>{item.name}</p>
                      <p>${item.price}</p>
                    </li>
                    <hr />
                  </ul>
                ))
              ) : <p>Your list is empty</p>
            }
          </div>
        </section>
      </React.Fragment>
    )
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

As items are added to the list, we need to update the state of the list to reflect those new items. To update the state of list using setState(), we’ll have to do this:

handleSubmit = (e) => {
  e.preventDefault()
  const newItem = {
    id: uuid.v4(),
    name: this.state.name,
    price: this.state.price
  }
  this.setState({ list: [...this.state.list, newItem] })
};

If you have to update multiple states in the application, you’ll have to do a ton of spreading to create a new state using the old state and the additional value. Which can look more complex as the number of changes increases. With Immer, it becomes very easy to do that, as we did in the example above.

What if we want to add a function that gets called as a callback after the state update?In this case, let’s say we are keeping a tally of the number of items in the list and the total price of all the items.

See the Pen
immer 3 - shopping list
by Kingsley Silas Chijioke (@kinsomicrote)
on CodePen.

Say we want to calculate the amount that will be spent based on the price of items in the list, we can have the handleSubmit function look like this:

handleSubmit = (e) => {
  e.preventDefault()
  const newItem = {
    id: uuid.v4(),
    name: this.state.name,
    price: this.state.price
  }
  
  this.setState(
    produce(draft => {
      draft.list = draft.list.concat(newItem)
    }), () => {
      this.calculateAmount(this.state.list)
    }
  )
};

First, we create an object using the data entered by the user, which we then assign to newItem. To update our application’s state, we make use of .concat() which will return a new array that's comprised of the previous items and the new item. This updated copy is now set as the value of draft.list, which can then be used by Immer to update the state of the application.

The callback function gets called after the state update. It’s important to note that it makes use of the updated state.

The function we want to call will look like this:

calculateAmount = (list) => {
  let total = 0;
    for (let i = 0; i < list.length; i++) {
      total += parseInt(list[i].price, 10)
    }
  this.setState(
    produce(draft => {
      draft.totalAmount = total
    })
  )
}

Let’s look at Immer hooks

use-immer is a hook that allows you to manage state in your React application. Let’s see this in action using a classic counter example.

import React from "react";
import {useImmer} from "use-immer";

const Counter = () => {
  const [count, updateCounter] = useImmer({
    value: 0
  });

  function increment() {
    updateCounter(draft => {
      draft.value = draft.value +1;
    });
  }

  return (
    <div>
      <h1>
        Counter {count.value}
      </h1>
      <br />
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default Counter;

useImmer is similar to useState. The function returns the state and an updater function. When the component loads at first, the value of the state (which is count in this example), is the same as the value passed to useImmer. Using the updater function which is returned, we can then create an increment function to increase the value of the count.

There is also a useReducer-like hook for Immer.

import React, { useRef } from "react";
import {useImmerReducer } from "use-immer";
import uuidv4 from "uuid/v4"
const initialState = [];
const reducer = (draft, action) => {
  switch (action.type) {
    case "ADD_ITEM":
      draft.push(action.item);
      return;
    case "CLEAR_LIST":
      return initialState;
    default:
      return draft;
  }
}
const Todo = () => {
  const inputEl = useRef(null);
  const [state, dispatch] = useImmerReducer(reducer, initialState);
  
  const handleSubmit = (e) => {
    e.preventDefault()
    const newItem = {
      id: uuidv4(),
      text: inputEl.current.value
    };
    dispatch({ type: "ADD_ITEM", item: newItem });
    inputEl.current.value = "";
    inputEl.current.focus();
  }
  
  const handleClear = () => {
    dispatch({ type: 'CLEAR_LIST' })
  }
  
  return (
    <div className='App'>
      <header className='App-header'>
        <ul>
          {state.map(todo => {
            return <li key={todo.id}>{todo.text}</li>;
          })}
        </ul>
        <form onSubmit={handleSubmit}>
          <input type='text' ref={inputEl} />
          <button
            type='submit'
          >
            Add Todo
          </button>
        </form>
        <button
          onClick={handleClear}
        >
          Clear Todos
        </button>
      </header>
    </div>
  );
}
export default Todo;

useImmerReducer takes in a reducer function and the initial state, and it returns both state and the dispatch function. We can then loop through the state to display the items we have. We dispatch an action when submitting a todo item and clearing the list of them. The dispatched action has a type which we use in determining what to do in the reducer function.
In the reducer function, we make use of draft like we did before, instead of state. With that, we have a convenient way of manipulating the state of our application.

You can find the code used in the above example on GitHub.

That’s a look at Immer!

Going forward, you can begin to make use of Immer in your next project, or even slowly begin to use it in the current project you’re working on. It has proven to aid in making state management convenient.

The post Using Immer for React State Management appeared first on CSS-Tricks.

Getting Started with React Testing Library

I can guess what you are thinking: another React testing library? So many have already been covered here on CSS-Tricks (heck, I’ve already posted one covering Jest and Enzyme) so aren’t there already enough options to go around?

But react-testing-library is not just another testing library. It’s a testing library, yes, but one that’s built with one fundamental principle that separates it from the rest.

The more your tests resemble the way your software is used, the more confidence they can give you.

It tries to address tests for how a user will use your application. In fact, it’s done in such a way that tests won’t break even when you refactor components. And I know that’s something we’ve all run into at some point in our React journey.

We’re going to spend some time writing tests together using react-testing-library for a light to-do application I built. You can clone the repo locally:

git clone https://github.com/kinsomicrote/todoapp-test.git

And, if you do that, install the required packages next:

## yarn
yarn add --dev react-testing-library jest-dom

## npm
npm install --save-dev react-testing-library jest-dom

In case you’re wondering why Jest is in there, we’re using it for assertion. Create a folder called __test__ inside the src directory and create a new file called App.test.js.

Taking snapshots

Snapshot tests keep a record of tests that have been performed on a tested component as a way to visually see what’s changes between changes.

When we first run this test, we take the first snapshot of how the component looks. As such, the first test is bound to pass because, well, there’s no other snapshot to compare it to that would indicate something failed. It only fails when we make a new change to the component by adding a new element, class, component, or text. Adding something that was not there when the snapshot was either created or last updated.

The snapshot test will be the first test we will be writing here. Let’s open the App.test.js file and make it look like this:

import React from 'react';
import { render, cleanup } from "react-testing-library";
import "jest-dom/extend-expect";
import App from './App';

afterEach(cleanup);

it("matches snapshot", () => {
  const { asFragment } = render(<App />);
  expect(asFragment()).toMatchSnapshot();
});

This imports the necessary packages we are using to write and run the tests. render is used to display the component we want to test. We make use of cleanup to clear things out after each test runs — as you can see with the afterEach(cleanup) line.

Using asFragment, we get a DocumentFragment of the rendered component. Then we expect it to match the snapshot that had been created.

Let’s run the test to see what happens:

## yarn
yarn test

## npm
npm test

As we now know, a snapshot of the component gets created in a new folder called __snapshots__ inside the __tests__ directory if this is our first test. We actually get a file called App.test.js.snap in there that will look like this:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`matches snapshot 1`] = `
<DocumentFragment>
  <div
    class="container"
  >
    <div
      class="row"
    >
      <div
        class="col-md-6"
      >
        <h2>
          Add Todo
        </h2>
      </div>
    </div>
    <form>
      <div
        class="row"
      >
        <div
          class="col-md-6"
        >
          <input
            class="form-control"
            data-testid="todo-input"
            placeholder="Enter a task"
            type="text"
            value=""
          />
        </div>
      </div>
      <div
        class="row"
      >
        <div
          class="col-md-6"
        >
          <button
            class="btn btn-primary"
            data-testid="add-task"
            type="submit"
          >
            Add Task
          </button>
        </div>
      </div>
    </form>
    <div
      class="row todo-list"
    >
      <div
        class="col-md-6"
      >
        <h3>
          Lists
        </h3>
        <ul
          data-testid="todos-ul"
        >
          <li>
            <div>
              Buy Milk
              <button
                class="btn btn-danger"
              >
                X
              </button>
            </div>
          </li>
          <li>
            <div>
              Write tutorial
              <button
                class="btn btn-danger"
              >
                X
              </button>
            </div>
          </li>
        </ul>
      </div>
    </div>
  </div>
</DocumentFragment>
`;

Now, let’s Test DOM elements and events

Our app includes two to-do items that display by default the first time the app runs. We want to make sure that they do, in fact, show up by default on the first app run so, to test this, we have to target the unordered list (<ul>) and check the length. We expect the length to be equal to two — the number of items.

it('it displays default todo items', () => {
  const { getByTestId } = render(<App />);
  const todoList = getByTestId('todos-ul');
  expect(todoList.children.length).toBe(2);  
});

We’re making use of getByTestId in that snippet to extract the test IDs from the App component. We then set todoList to target the todos-ul element. That’s what should return as two.

Using what we’ve learned so far, see if you can write a test to assert that a user can enter values in the input field. Here are the things you’ll want to do:

  • Get the input field
  • Set a value for the input field
  • Trigger a change event
  • Assert that the input field has its value as the one you set for it in Step 2

Don’t peek at my answer below! Take as much time as you need.

Still going? Great! I’ll go grab some coffee and be right back.

Mmm, coffee. ☕️

Oh, you’re done! You rock. Let’s compare answers. Mine looks like this:

it('allows input', () => {
  const {getByTestId } = render(<App />)
  let item = 'Learn React'
  const todoInputElement = getByTestId('todo-input');
  todoInputElement.value = item;
  fireEvent.change(todoInputElement);
  expect(todoInputElement.value).toBe('Learn React')
});

Using getByTestId, I am able to extract the test IDs in the application. Then I create a variable which is set to the string Learn React, and make it the value of the input field. Next, I obtain the input field using its test ID and fire the change event after setting the value of the input field. With that done, I assert that the value of the input field is indeed Learn React.

Does that check out with your answer? Leave a comment if you have another way of going about it!

Next, let’s test that we can add a new to-do item. We’ll need to get the input field, the button for adding new items and the unordered list because those are all of the elements needed to create an new item.

We set a value for the input field and then trigger a button click to add the task. We’re able to do this by obtaining the button using getByText — by triggering a click event on the DOM element with the text Add Task, we should be able to add a new to-do item.

Let’s assert that the number of children (list items) in unordered list element is equal to three. This assumes that the default tasks are still in tact.

it('adds a new todo item', () => {
  const { getByText, getByTestId } = render(<App />);
  const todoInputElement = getByTestId('todo-input');
  const todoList = getByTestId('todos-ul');
  todoInputElement.value = 'Learn React';
  fireEvent.change(todoInputElement);
  fireEvent.click(getByText('Add Task'))
  expect(todoList.children.length).toBe(3); 
});

Pretty nice, right?

This is just one way to test in React

You can try react-testing-library in your next React application. The documentation in the repo is super thorough and — like most tools — the best place to start. Kent C. Dodds built it and has a full course on testing over at Frontend Masters (subscription required) that also covers the ins and outs of react-testing-library.

That said, this is just one testing resource for React. There are others, of course, but hopefully this is one you’re interested in trying out now that you’ve seen a bit of it but use what’s best for your project, of course.

The post Getting Started with React Testing Library appeared first on CSS-Tricks.