Managing React State with Render Props

Brief Introduction

Over the years, React has evolved with different patterns and techniques to solve 2 fundamental problems:

  1. How to share and reuse code?
  2. How to manage unidirectional state?

In early versions of React there was the concept of mixins. These ended up being a fragile solution. Mixins were eventually deprecated and a new pattern emerged called Higher-Order Components (HoC). A HoC helps solve our first issue of sharing and reusing code in a maintainable and composable manner.

Here is the definition from the React documentation:

A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from Reacts compositional nature.

Facebook also released a pattern called Flux. Flux manages state with unidirectional data flow. A library called Redux evolved from the ideas of Flux and quickly became one of the de facto ways to manage state in React applications. It became common for developers to manage their entire application state in Redux and use HoC for code reuse.

Redux is a great library but what if there was a way to manage state without using a separate library? Well luckily React supports built-in local component state. Using local state and a technique called render props we can share and reuse code as well as manage unidirectional state using only React components. Render props offer a way to reuse code by encapsulating state entirely in React. As of React 16.3, render props offer another advantage – React released an official context API which uses a flavour of render props called function as a child pattern.

Lets Build A Render Prop Component!

A simple definition of a render prop from the React documentation is:

The term “render prop” refers to a simple technique for sharing code between React components using a prop whose value is a function.

The best way to illustrate this is through an example.

Let’s pretend you are tasked with building an Adder component. This component needs to take an initial value that is the current value of the Adder. Every time a user submits a new value, it adds the submitted value with the current value of the Adder. The component might look something like this:

import React from 'react'

export default class Adder extends React.Component {
  inputEl = React.createRef()

  state = {
    value: this.props.initialValue
  }

  static defaultProps = {
    initialValue: 0
  }

  handleAddValue = event => {
    event.preventDefault()

    this.setState(
      state => ({
        value: state.value + Number(this.inputEl.current.value)
      }),
      () => (this.inputEl.current.value = '')
    )
  }

  render() {
    return (
      <div>
        <div>My value is {this.state.value}</div>
        <input type="number" ref={this.inputEl} />
        <button type="button" onClick={this.handleAddValue}>
          Add Value
        </button>
      </div>
    )
  }
}

Original Adder Component (CodeSandbox)

The component takes an initialValue prop or defaults to 0. Every time the button is clicked, the value of the input is added to the state value.

Everything is working great; then your co-worker comes along and wants to reuse your Adder component but with the caveat that it also needs to handle other math operations like subtraction, multiplication and division; as well as being able to support a different look and feel.

Since the styling may differ between the components, the most important part of the code to reuse is the state of the component. This means the state object which holds our value as well as the methods that update the state. Before we begin to convert this component to use render props, it’s important to understand how JSX works when its transpiled.

Here is a simple render function:

render() {
  return (
    <button disabled={this.props.disabled}>
      <span>{this.props.icon}</span>
      {this.props.text}
    </button>
  )
}

When Babel compiles the JSX it becomes:

render() {
  return React.createElement(
    "button",
    { disabled: this.props.disabled },
    React.createElement("span", null, this.props.icon),
    this.props.text
  );
}

Babel takes JSX and converts it to React.createElement which has the following signature:

React.createElement(type, [props], [...children])

What would happen if instead of returning JSX we returned a function from the render function?

render() {
  return this.props.render({ some: 'values' })
}

Then in the parent component, we can pass the component a render prop:

render() {
  return <MyComponent render={(obj) => (...)} />
}

When this is compiled by Babel it looks like:

render() {
  return React.createElement(MyComponent, {
    render: obj => { ... }
  });
}

Notice how now the props object (the second argument) returns a function that expects an obj as an argument. This obj populates when the component render method is executed. This means the obj value above receives {some:'values'} as an argument. This object can be destructured to be written as:

render() {
  return <MyComponent render={({ some }) => <h1>{some}</h1>} />
}

Now that we have a better understanding of how JSX and render props work, let’s update our Adder component by calling our render prop with our local state object:

import React from 'react'

export default class Adder extends React.Component {
  inputEl = React.createRef()

  state = {
    value: this.props.initialValue
  }

  static defaultProps = {
    initialValue: 0
  }

  handleAddValue = event => {
    event.preventDefault()

    this.setState(
      state => ({
        value: state.value + Number(this.inputEl.current.value)
      }),
      () => (this.inputEl.current.value = '')
    )
  }

  render() {
    return this.props.render(this.state)
  }
}

Note: We are using a prop called render in our example, but any prop can be a render prop!

We can then create a new component called AdderView:

export default class AdderView extends React.Component {
  render() {
    return (
      <Adder
        render={({ value }) => (
          <div>
            <div>My value is {value}</div>
            <input type="number" ref={el => (this.inputEl = el)} />
            <button type="button" onClick={this.handleAddValue}>
              Add Value
            </button>
          </div>
        )}
      />
    )
  }
}

Notice how this.state is passed to the render prop.

This means we can destructure on the state object like our first introduction example because the state shape is:

{
  value: ...
}

This is great! We have now decoupled our view and the state of the component. However, there is still one problem. We had a method called handleAddValue which the child component no longer has access to on its instance. If you click Add Value, it has no way to update the parent state.

We need a way for the child component to tell the parent component to update without breaking one-way data flow. Since we are using local state, we need a way for the child component to tell the parent component to call setState. One way to solve this is to create a function in the parent component and add it as a property on the object passed to our render prop. Then whenever the child component needs to update state, it calls this function.

This function then executes in the parent context and calls setState. Once setState is run, if any state value has changed, those new values propagate down into our render prop and the child component now receives the new value.

This preserves our unidirectional data flow. Let’s make those updates now:

render() {
  return this.props.render({
    ...this.state,
    addValue: this.handleAddValue
  })
}

We can tweak the current handleAddValue:

handleAddValue = value => {
  this.setState(state => ({
    value: state.value + value
  }))
}

Also tweak our new AdderView component:

export default class AdderView extends React.Component {
  inputEl = React.createRef()

  render() {
    return (
      <Adder
        render={({ value, addValue }) => {
          return (
            <div>
              <div>My value is {value}</div>
              <input type="number" ref={this.inputEl} />
              <button
                type="button"
                onClick={e => {
                  e.preventDefault()

                  addValue(Number(this.inputEl.current.value))
                  this.inputEl.current.value = ''
                }}
              >
                Add Value
              </button>
            </div>
          )
        }}
      />
    )
  }
}

Now our values object that is passed into the render function is:

{
  value: ...,
  addValue: value => {...}
}

Let’s take a step back and think about this object and how it relates to our component. We now have an object literal representation of our state, with a function to update that state. This is a big advantage to using render props. You can see a clear snapshot of the state of a component in a plain Javascript object.

Here is the final render prop Adder component (CodeSandbox)

Now back to the original problem. We have a component which manages our state and passes that state as an argument to our render prop; meaning we can now consume any view necessary. This solves one of the re-usability issues we had. Now, we just need to add the other math operations – subtraction, multiplication and division.

Since our component name is too specific for the new features we want to implement, let’s first rename our component from Adder to Math and add the new actions. Let’s also add a method which returns the actions as an object and returns all actions under the same object property. This makes it clear to your co-worker which values are coming from state, and which values are functions to update the parent state. To echo with Redux terminology, let’s call this property actions.

export default class Math extends React.Component {
  state = {
    value: this.props.initialValue
  }

  static defaultProps = {
    initialValue: 0
  }

  handleResetValue = () => {
    this.setState({
      value: this.props.initialValue
    })
  }

  handleAddValue = value => {
    this.setState(state => ({
      value: state.value + value
    }))
  }

  handleSubtractValue = value => {
    this.setState(state => ({
      value: state.value - value
    }))
  }

  handleMultiplyValue = value => {
    this.setState(state => ({
      value: state.value * value
    }))
  }

  handleDivideValue = value => {
    if (value !== 0) {
      this.setState(state => {
        return {
          value: state.value / value
        }
      })
    }
  }

  getActions = () => {
    return {
      resetValue: this.handleResetValue,
      addValue: this.handleAddValue,
      subtractValue: this.handleSubtractValue,
      multiplyValue: this.handleMultiplyValue,
      divideValue: this.handleDivideValue
    }
  }

  render() {
    return this.props.render({
      ...this.state,
      actions: this.getActions()
    })
  }
}

Now when the render prop is called it contains a state object that looks like this:

{
  value: ...,
  actions: {
    resetValue: value => {...},
    addValue: value => {...},
    subtractValue: value => {...},
    multiplyValue: value => {...},
    divideValue: value => {...}
  }
}

Our co-worker can now take this component and reuse all the state logic while rendering the same view or a different view. The state of the component is also encapsulated entirely in this component, meaning the component can render multiple times on the same page without any issues. For example:

render() {
  const style = { padding: '1em' }

  return (
    <div>
      <Math
        render={({ value, actions }) => (
          <Adder value={value} addValue={actions.addValue} />
        )}
      />
      <div style={style} />
      <Math render={mathProps => <AllOperations {...mathProps} />} />
    </div>
  )
}

This displays two different components that contain a unique state instance in each component.

Multiple Render Prop Components

The full example is here (CodeSandbox)

Sharing State with Context

One question you may still have is what if you wanted to share the values from a single Math component instance, where each component may have different parents. This is where React Context can help. (If you are not familiar with Context, the official React documentation are a great place to start). React documentation describes Context as:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

A Context can be thought of as a container of encapsulated state that can be shared with an entire subtree. As mentioned earlier, Context uses function as a child which is a type of render prop; but instead of using a prop called render, we use the children prop which is the value between the Element / Component opening and closing tags in JSX.

A Context is made up of two Components – a Provider component and a Consumer component. The Provider sets a value at the top of the component tree and broadcasts this value to any child Consumer component. To understand this more lets first create a Provider component:

const MathContext = React.createContext()

export class MathContextProvider extends React.Component {
  render() {
    return (
      <Math
        render={values => (
          <MathContext.Provider value={values}>
            {this.props.children}
          </MathContext.Provider>
        )}
      />
  )
}

As you can see, since both Context and our Math component use render props, we can simply wrap the Provider with our Math component. Whenever our Math component state changes, it passes new values to the Provider which then broadcasts this object to any Consumers. Our Consumer component looks like this:

import React from 'react'

const MathContext = React.createContext()

class MathContextProvider extends React.Component {
  render() {
    return (
      <Math
        render={values => (
          <MathContext.Provider value={values}>
            {this.props.children}
          </MathContext.Provider>
        )}
      />
    )
  }
}
class MathContextConsumer extends React.Component {
  render() {
    return (
      <MathContext.Consumer>
        {mathProps => (
          ... // consume the mathProps object
        )}
      </MathContext.Consumer>
    )
  }
}

See a full working example here (CodeSandbox)

Summary

Does this mean you should throw out your old Redux code or HoCs and convert them all to render props? Well no, if your code is working and you are happy with it then there is no reason to change. The best pattern or library depends entirely on the use case!

How do render props compare to Redux when managing state? A core philosophy in Redux is having a single source of truth. This makes reads and updates to the global store predictable. This can be a huge advantage in server rendering when generating an initial state to hydrate an application or optimize to only re-render when the state changes. However, the predictability comes with a certain level of ceremony around writing this code. On the other hand, render props handle reads and updates to state with React local state. It is trickier to apply rendering optimizations to render props than Redux. It defers any optimizations to the React reconciler to handle the updates and re-render accordingly. Render props require no additional libraries or configuration to be added to an application. They work well in existing applications because the state is encapsulated entirely inside a component.

What if you are happy with HoCs? Why should you consider using render props at all? Well, HoC have some caveats that a seasoned React developer most certainly encounters. The caveats require you to handle edge cases that arise around wrapping the original component with a container component. These issues are avoided with render props because you are only passing props. The other great thing is that HoCs can be built with render prop components. Check out the react-router withRouter HoC for an example on what that looks like.

Render props provide a flexible structure for building components in React by decoupling state and the view. They can be used to build HoCs as well as Context components. They require no additional libraries or configuration to be added to existing React applications because they use local state.

I highly recommend exploring render props with your own projects today! If you are interested in learning more about render props check out this list of great open source projects using render props!

Top