Immutable Deep State Updates in React with Ramda.js
Tags: react, ramda, javascript, functional
Published: 2017-07-09 01:48:42

---
title: Immutable Deep State Updates in React with Ramda.js
published: true
description: In this post, I'm going to show you how you can leverage lenses to handle deep state updates in a clean and functional way.
tags: react, ramda, javascript, functional
canonical_url: https://vanslaars.io/blog/setstate-lenses
---

Basic state updates in React are a breeze using `setState`, but updating deeply nested values in your state can get a little tricky. In this post, I'm going to show you how you can leverage lenses in [Ramda](http://ramdajs.com/) to handle deep state updates in a clean and functional way.

Let's start with a simple counter component.

```js
import React from 'react';
import { render } from 'react-dom';

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
    this.increase = this.increase.bind(this)
    this.decrease = this.decrease.bind(this)
  }

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

  decrease() {
    this.setState((state) => ({count: state.count - 1}))
  }

  render() {
    return (
      <div>
        <button onClick={this.increase}>+</button>
        <div>
          {this.state.count}
        </div>
        <button onClick={this.decrease}>-</button>
      </div>
    )
  }
}

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

Here, we're using a function as the argument for `setState` and just incrementing or decrementing the count based on the passed in state value. This is fine for a simple property sitting at the top level of the state tree, but let's update the shape of our state object and move that `count` a little deeper into the state.

```js
this.state = {
  a: {
    name: 'pointless structure',
    b : {
      stuff: 'things',
      count: 0
    }
  }
}
```

This new `state` is incredibly contrived, but it'll help illustrate the point. Now, in order to update the count, we need to update property `a`, which in turn needs an updated `b` and that will contain our updated `count`. The update function for `increase` will now need to look like this:

```js
increase() {
  this.setState((state) => ({a: {...state.a, b: {...state.a.b, count: state.a.b.count + 1}} }))
}
```

This works, but is not very readable. Let's briefly look at what's happening here.

The existing state is passed into the function, and we want to return an object that represents the object to be merged with `state`. The `setState` method doesn't merge recursively, so doing something like `this.setState((state) => ({a: {b:{ count: state.a.b.count + 1}}}))` would update the count, but the other properties on `a` and `b` would be lost. In order to prevent that, the returned object is created by spreading the existing properties of `state.a` into a new object where we then replace `b`. Since `b` also has properties that we want to keep, but don't want to change, we spread `state.b`'s props and replace just `count`, which is replaced with a new value based on the value in `state.a.b.count`.

Of course, we need to do the same thing with `decrease`, so now the entire component looks like this:

```js
import React from 'react';
import { render } from 'react-dom';

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      a: {
        name: 'pointless structure',
        b : {
          stuff: 'things',
          count: 0
        }
      }
    }
    this.increase = this.increase.bind(this)
    this.decrease = this.decrease.bind(this)
  }

  increase() {
    this.setState((state) => ({a: {...state.a, b: {...state.a.b, count: state.a.b.count + 1}} }))
  }

  decrease() {
    this.setState((state) => ({a: {...state.a, b: {...state.a.b, count: state.a.b.count - 1}} }))
  }

  render() {
    return (
      <div>
        <h1>{this.state.a.name}</h1>
        <h2>{this.state.a.b.stuff}</h2>
        <button onClick={this.increase}>+</button>
        <div>
          {this.state.a.b.count}
        </div>
        <button onClick={this.decrease}>-</button>
      </div>
    )
  }
}

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

Those `setState` calls are kind of a mess! The good news is, **there's a better way**. Lenses are going to help us clean this up and get back to state updates that are both readable and clearly communicate the intent of the update.

Lenses allow you to take an object and "peer into it", or "focus on" a particular property of that object. You can do this by specifying a path to put your focus on a property that is deeply nested inside the object. With that lens focused on your target, you can then set new values on that property without losing the context of the surrounding object.

To create a lens that focuses on the `count` property in our state, we will use ramda's `lensPath` function and array that describes the path to `count`, like so:

```js
import {lensPath} from 'ramda'

const countLens = lensPath(['a', 'b', 'count'])
```

Now that we have a lens, we can use it with one of the lens-consuming functions available in ramda: `view`, `set` and `over`. If we run `view`, passing it our lens and the state object, we'll get back the value of `count`.

```js
import {lensPath, view} from 'ramda'
const countLens = lensPath(['a', 'b', 'count'])
// somewhere with access to the component's state
view(countLens, state) // 0
```

Admittedly, `view` doesn't seem super useful since we could have just referenced the path to `state.a.b.count` or use ramda's `path` function. Let's see how we can do something useful with our lens. For that, we're going to use the `set` function.

```js
import {lensPath, view, set} from 'ramda'
const countLens = lensPath(['a', 'b', 'count'])
// somewhere with access to the component's state
const newValue = 20
set(countLens, newValue, state)
```

When we do this, we'll get back an object that looks like:

```js
{
  a: {
    name: 'pointless structure',
    b : {
      stuff: 'things',
      count: 20 // update in context
    }
  }
}
```

We've gotten back a new version of our `state` object in which the value of `state.a.b.count` has been replaced with `20`. So not only have we made a targeted change deep in the object structure, we did it in an immutable way!

So if we take what we've learned so far, we can update our `increment` method in our component to look more like this:

```js
increase() {
    this.setState((state) => {
      const currentCount = view(countLens, state)
      return set(countLens, currentCount+1, state)
    })
  }
```

We've used `view` with our lens to get the current value, and then called `set` to update the value based on the old value and return a brand new version of our entire `state`.

We can take this a step further. The `over` function takes a lens and a function to apply to the target of the lens. The result of the function is then assigned as the value of that target in the returned object. So we can use ramda's `inc` function to increment a number. So now we can make the `increase` method look like:

```js
increase() {
    this.setState((state) => over(countLens, inc, state))
  }
```

Pretty cool, right?! Well, it gets even better... no, for real, it does!

All of ramda's functions are automatically curried, so if we pass `over` just the first argument, we get back a new function that expects the second and third arguments. If I pass it the first two arguments, it returns a function that expects the last argument. So that means that I can do this:

```js
increase() {
    this.setState((state) => over(countLens, inc)(state))
  }
```

Where the initial call to `over` returns a function that accepts `state`. Well, `setState` accepts a function that accepts `state` as an argument, so now I can shorten the whole thing down to:

```js
increase() {
    this.setState(over(countLens, inc))
  }
```

And if this doesn't convey enough meaning for you, you can move that `over` function out of the component and give it a nice meaningful name:

```js
// outside of the component:
const increaseCount = over(countLens, inc)

// Back in the component
increase() {
    this.setState(increaseCount)
  }
```

And of course, the same can be done to the `decrease` method using `dec` from ramda. This would make the whole setup for this component look like this:

```js
import React from 'react';
import { render } from 'react-dom';
import {inc, dec, lensPath, over} from 'ramda'

const countLens = lensPath(['a', 'b', 'count'])
const increaseCount = over(countLens, inc)
const decreaseCount = over(countLens, dec)

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      a: {
        name: 'pointless structure',
        b : {
          stuff: 'things',
          count: 0
        }
      }
    }
    this.increase = this.increase.bind(this)
    this.decrease = this.decrease.bind(this)
  }

  increase() {
    this.setState(increaseCount)
  }

  decrease() {
    this.setState(decreaseCount)
  }

  render() {
    return (
      <div>
        <h1>{this.state.a.name}</h1>
        <h2>{this.state.a.b.stuff}</h2>
        <button onClick={this.increase}>+</button>
        <div>
          {this.state.a.b.count}
        </div>
        <button onClick={this.decrease}>-</button>
      </div>
    )
  }
}

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

The nice thing here is that if the shape of the state changes, we can update our state manipulation logic just by adjusting the `lensPath`. In fact, we could even use the lens along with `view` to display our data in `render` and then we could rely on that `lensPath` to handle **all** of our references to count!

So that would mean this: `{this.state.a.b.count}` would be replaced by the result of: `view(countLens, this.state)` in the `render` method.

So here it is with that final adjustment, take it for a spin and see what you can do with it!

[![Edit Ramda Lenses - React setState](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/Yz5Bkz3n)

<iframe src="https://codesandbox.io/embed/Yz5Bkz3n?view=editor" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:auto;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>