react-state-mutations

February 22, 2019 - @kddeisz

There are two types of objects that you can pass to the first argument to setState within React components. The first is an object, which will update the state to be equal to that value, as in:

this.setState({ count: 0 });

The second is a function, which will be called with the current state and props, and should return an object that will then be used to set the state, as in:

this.setState(() => ({ count: 0 }));

In this example, both are equivalent. However, things start to get interesting when you need access to the previous state to calculate the new state (e.g., if you were adding one to the previous count). Because React state updates may be asynchronous and the state updates are merged, it’s possible that you could end up with a race condition. For example:

this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });

In the above example, because the updates can be asynchronous and they are merged, it’s possible that the second update could be merged into the first and you would wind up with the count only being incremented by one. This doesn’t happen with the function form of the argument, as in:

this.setState(({ count }) => ({ count: count + 1 }));
this.setState(({ count }) => ({ count: count + 1 }));

These updates are performed in sequence, and so the count variable that you’re pulling from the previous state is always guarunteed to be up to date.

Mutations

An added benefit of using the function variant is that you can isolate the state mutation and reuse it in multiple places, as in:

const incrementCount = ({ count }) => ({ count: count + 1 });

this.setState(incrementCount);
this.setState(incrementCount);

We can now use incrementCount anywhere we’d like in our application, without having to redeclare the function. This ensures a certain consistency within our application and a very tiny amount of memory reduction.

We can make these kinds of mutations even more generic by accepting the name of the field we’re modifying so that it could be used for any field, as in:

const increment = name => state => ({ [name]: state[name] + 1 });
const incrementCount = increment("count");

Now we can reuse increment throughout our application whenever anything needs incrementing, and it doesn’t matter the name of the value in the state object.

react-state-mutations

We’ve built a library called react-state-mutations that encapsulates simple mutations like the one above into functions like increment. Examples for all of the below mutations can be found in the README of the repository.

Standalone

There are “standalone” mutations like increment that function on just an initial value. These include:

Argument

There are also “argument” mutations that function on an initial value and an additional value for each mutation.

Within this category include mutations that work on adding and removing elements from lists:

As well as mutations that modify lists:

Finally, there are two special mutations for specific use cases:

Combinations

One of the more powerful features of this library is that all of these mutations can be combined to perform multiple mutations using one function through the combineMutations function. For example, if you wanted to toggle a value and increment a count in the same mutation, you could:

import React, { Component } from "react";
import { combineMutations, append, toggle } from "react-state-mutations";

const toggleFeature = combineMutations(
  append("eventLog"),
  toggle("featureEnabled")
);

class FeatureFlag extends Component {
  state = {
    eventLog: [],
    featureEnabled: false
  };

  handleClick = () => {
    this.setState(toggleFeature(new Date()));
  };

  render() {
    const { eventLog, featureEnabled } = this.state;

    return (
      <>
        <button type="button" onClick={this.handleClick}>
          {featureEnabled ? "Enabled" : "Disabled"}
        </button>
        <ul>
          {eventLog.map(event => {
            <li key={+event}>{event}</li>
          })}
        </ul>
      </>
    );
  }
}

The above component functions as a toggle and keeps track of the times that the button is clicked. combineMutations combines the functionality of each of the passes mutations, and passes the arguments on to the appropriate “argument” mutations.

Hooks

Recently, we added support for React’s hooks by allowing our state mutations to be used as individual hooks. For example, the increment equivalent is useIncrement, as in:

import React from "react";
import { useIncrement } from "react-state-mutations";

const Counter = () => {
  const [count, onIncrement] = useIncrement();

  return <button type="button" onClick={onIncrement}>{count}</button>;
};

Each of the hooks returns a two-element array (mirroring the useState hook). The first element is the current value, and the second element is a function that can be called to mutate the state.

This functions similarly with “argument” mutations, as in:

import React from "react";
import { useAppend } from "react-state-mutations";

const ClickLog = () => {
  const [events, onAppend] = useAppend([]);

  return (
    <>
      <button type="button" onClick={() => onAppend(new Date())}>Click</button>
      <ul>
        {events.map(event => (
          <li key={+event}>{event}</li>
        ))}
      </ul>
    </>
  );
};

Build your own

You can even create your own hooks using makeStandaloneHook and makeArgumentHook. For example, you could write something that doubles in value each time:

import React from "react";
import { makeStandaloneHook } from "react-state-mutations";

const useDouble = makeStandaloneHook(value => value * 2, 1);

const DoubleDouble = () => {
  const [count, onDouble] = useDouble();

  return <button type="button" onClick={onDouble}>{count}</button>;
};

Or you could write a hook that keeps track of a sum:

import React, { useCallback } from "react";
import { makeArgumentHook } from "react-state-mutations";

const useSum = makeArgumentHook(object => value => value + object, 0);

const Sum = () => {
  const [count, onAdd] = useSum(0);
  const [num, setNum] = useState("");

  const onChange = useCallback(event => setNum(event.target.value), []);
  const onClick = useCallback(() => onAdd(num), [num]);

  return (
    <>
      <input type="number" value={num} onChange={onChange} />
      <button type="button" onClick={onClick}>{count}</button>
    </>
  );
};

tl;dr

We built a library called react-state-mutations that handles mutating React state objects without race conditions. It leads to more code reuse and fewer bugs. We also now have support for hooks that further enhances this capability.

← Back to home