Company logo

We use cookies to personalize our service and to improve your experience on the website and its subdomains. We also use this information for analytics.

More info

React Patterns

It's great to see how developers are constantly discovering new patterns in the React ecosystem. Some kind of pattern that you once considered an anti-pattern suddenly gets a name, and developers start using it. But there are well-established models that have been used for a long time - let's look at them.

High Order Component

Let's assume that we have the following component:

class App extends Component {
  state = {
    sidebarIsOpen: get('sidebarIsOpen', true)
  }

  componentDidMount() {
    this.unsubscribe = subscribe(() => {
      this.setState({
        sidebarIsOpen: get('sidebarIsOpen')
      })
    })
  }

  componentWillUnmount() {
    this.unsubscribe()
  }

  handleChangeSidebarState = () => {
    const { sidebarIsOpen } = this.state
    set('sidebarIsOpen', !sidebarIsOpen)
  }

  render() {
    const { sidebarIsOpen } = this.state
    return (
      <div className="app">
        <header>
         <button
          onClick={this.handleChangeSidebarState}
         >
           Toggle menu
         </button>
        </header>
        <div className="container">
          <aside className={sidebarIsOpen ? 'open' : 'closed'} />
          <main />
        </div>
      </div>
    );
  }
}

export default App;

In order not to be misleading, we define that the get, set and subscribe methods are just helpers for working with localStorage:

let subscribers = []

export const set = (key, value) => {
  localStorage.setItem(key, value)
  subscribers.forEach(s => s())
}

export const subscribe = (fn) => {
  subscribers.push(fn)
  const unsubscribe = () => {
    subscribers.filter(s => s !== fn)
  }
  return unsubscribe
}

export const get = (key, default_) => (
  JSON.parse(localStorage.getItem(key) || JSON.stringify(default_))
)

What problem will we face if we take a closer look at the App component?
Firstly, those things that relate to the sidebar should not concern the App - you need to try to keep it as clean as possible.
Secondly, such logic may be required by us not only in the App component, therefore we must endure this logic.

Let's try this with HOC.

There are only three tasks that we must execute:

  1. Rid the App of logic related to localStorage.
  2. Create a HOC withStorage, with which the App will get all the necessary data and methods to manage this data.
  3. Ensure that withStorage is flexible and can be used anywhere in our application.

Let's start!

And we will start with the second paragraph - it will be much easier to follow the changes in our application.
Let's start writing withStorage. What function should perform withStorage? What useful should he be able to?

WithStorage should take on everything related to working with localStorage. This means that the components that “turn around” in withStorage should receive the data that they request, as well as the methods from localStorage.

Let's try:

const withStorage = (key, default_) => (Comp) => (
  class WithStorage extends Component {
    state = {
      [key]: get(key, default_)
    }

    componentDidMount() {
      this.unsubscribe = subscribe(() => {
        this.setState({
          [key]: get(key)
        })
      })
    }

    componentWillUnmount() {
      this.unsubscribe()
    }

    render() {
      return (
        <Comp
          {...this.props}
          {...this.state}
          setStorage={set}
        />
      )
    }
  }
)

Let's take a closer look at what happens inside the repository:

  1. All that will be passed as the key parameter in withStorage will be in its state.
  2. Any component that will be in the Comp parameter will get the setStorage method and all the necessary values from localStorage as props.
  3. When unmounting a component, HOC withStorage will unsubscribe from any changes to localStorage.

This logic allows you to be extremely flexible withStorage, which means that it doesn’t matter exactly which fields from localStorage need to be obtained or changed - we determine this, and withStorage only provides information and tools for its management.

And finally, we will return to the first point - we need to clean the App. Remove some lifecycle hooks, state and change where the App should get data from - in our case, not from the state, but from the props. As well as methods for data management:

class App extends Component {
  handleChangeSidebarState = () => {
    const { sidebarIsOpen, setStorage } = this.props
    setStorage('sidebarIsOpen', !sidebarIsOpen)
  }

  render() {
    const { sidebarIsOpen } = this.props
    return (
      <div className="app">
        <header>
         <button
          onClick={this.handleChangeSidebarState}
         >
           Toggle menu
         </button>
        </header>
        <div className="container">
          <aside className={sidebarIsOpen ? 'open' : 'closed'} />
          <main />
        </div>
      </div>
    );
  }
}

export default App;

I think that the syntax of the call withStorage will not be so unexpected any more - using the example of an App, we only need to slightly change its usual export. Now it will look like this:

export default withStorage('sidebarIsOpen', true)(App)

It turns out that HOC is a “wrapper” over the component that we have transmitted, which expands its functionality and (or) provides additional data.

Compound Components

Like last time, let's immediately consider the problem by example. We have an App component:

class App extends Component {
  render() {
    return (
      <RadioGroup defaultValue="pause" legend="Radio Group">
        <RadioButton value="back">Back</RadioButton>
        <RadioButton value="play">Play</RadioButton>
        <RadioButton value="pause">Pause</RadioButton>
        <RadioButton value="forward">Forward</RadioButton>
      </RadioGroup>
    )
  }
}

and components RadioGroup and RadioButton:

class RadioGroup extends Component {
  render() {
    return (
      <fieldset className="radio-group">
        <legend>{this.props.legend}</legend>
        {this.props.children}
      </fieldset>
    )
  }
}

class RadioButton extends Component {
  render() {
    const isActive = false
    const className = `radio-button ${isActive ? 'active' : ''}`
    return (
      <button className={className}>
        {this.props.children}
      </button>
    )
  }
}

Now our buttons are not functioning, and our main task is to make them work!

We do not have to change the App component, but let's take a closer look at its render. As we can see, RadioGroup gets a legend (used only to draw a kind of “cap”), defaultValue, and a set of RadioButton components as children.

As you can see from the task, we need to initially define a different data store for RadioGroup and, not least, to somehow forward this data in RadioButton`s. In turn, when clicking on any of the buttons (RadioButton), the data inside the RadioGroup must be changed.

Let's create this data store inside the RadioGroup component:

class RadioGroup extends Component {
  state = {
    value: this.props.defaultValue
  }

  render() {
    return (
      <fieldset className="radio-group">
        <legend>{this.props.legend}</legend>
        {this.props.children}
      </fieldset>
    )
  }
}

So, our initialized value will depend on the passed parameter defaultValue

As can be seen from the render of the RadioButton component, isActive is “tough” for us. This needs to be fixed, because by clicking on different buttons, isActive will always change.
It turns out that we should receive isActive from props:

class RadioButton extends Component {
  render() {
    const { isActive } = this.props
    const className = `radio-button ${isActive ? 'active' : ''}`
    return (
      <button className={className}>
        {this.props.children}
      </button>
    )
  }
}

Now you need to make RadioButton get this isActive from RadioGroup. Here, React.Children and React.cloneElement come to the rescue to identify new props.

class RadioGroup extends Component {
  state = {
    value: this.props.defaultValue
  }

  render() {
    const children = React.Children.map(this.props.children, (child) => {
      return React.cloneElement(child, {
        isActive: child.props.value === this.state.value,
      })
    })
    return (
      <fieldset className="radio-group">
        <legend>{this.props.legend}</legend>
        {children}
      </fieldset>
    );
  }
}

Do you see what has changed? Instead of the standard this.props.children, we clone each child element, expanding its props. Thus, inside each RadioButton there will be two parameters - value (which was transmitted inside the App), and isActive (true, if the value inside RadioButton is equal to the value from the RadioGroup state).

Now, you need to write a function that will change the value from the state of RadioGroup. When will this function be called? When any of the buttons inside the RadioButton is clicked.
RadioButton does not have its own state, and if inside the component there is some kind of event that changes data that is not inside the state of this component, props is what we need. Let's add RadioButton a little:

class RadioButton extends Component {
  render() {
    const { isActive, onSelect } = this.props
    const className = `radio-button ${isActive ? 'active' : ''}`
    return (
      <button className={className} onClick={onSelect}>
        {this.props.children}
      </button>
    );
  }
}

and in the same way as isActive, we can pass onSelect to this component:

class RadioGroup extends Component {
  state = {
    value: this.props.defaultValue
  }

  render() {
    const children = React.Children.map(this.props.children, (child) => {
      return React.cloneElement(child, {
        isActive: child.props.value === this.state.value,
        onSelect: () => this.setState({ value: child.props.value })
      })
    })
    return (
      <fieldset className="radio-group">
        <legend>{this.props.legend}</legend>
        {children}
      </fieldset>
    );
  }
}

It seems that we coped with the task!

It turns out that even if the component has children among its props, this does not mean that it should render it. In our case, we were able to clone the children, and render something based on them.

Render Props

The render props pattern is somewhat similar to HOC in its essence — it also serves to gain flexibility and reuse components and their data.

I think you already guessed what we will do now. Not? Consider an example!

class App extends Component {
  state = {
    x: 0,
    y: 0,
  }

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  }

  render() {
    const { x, y } = this.state

    return (
      <div onMouseMove={this.handleMouseMove}>
        <h1>The mouse position is ({x}, {y})</h1>
      </div>
    )
  }
}

As you can see from the App, all that is happening here is working with the current position of the mouse relative to the screen. Any component can use this kind of logic, so I think it would be correct to put this into a separate component. We can do this with HOC, and I think you already know how. But let's try it with the render props pattern.

To begin with, we will move all the logic regarding the position of the mouse into a separate component, for example, Mouse:

class Mouse extends Component {
  state = {
    x: 0,
    y: 0,
  }

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  }

  render() {
    return (
      <div onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    )
  }
}

Already inside the render method you can see what the essence of the render props pattern is - we know that the render function will be passed to us as a parameter (it can be called anything - I chose the name render only by convention), and we determine which arguments will be passed into this function. Depending on where the Mouse component will be rendered, this data can be used in different ways. In the case of our App, it will look like this:

class App extends Component {
  render() {
    return (
      <div>
        <Mouse
          render={({ x, y }) => (
            <h1>The mouse position is ({x}, {y})</h1>
          )}
        />
      </div>
    )
  }
}

This pattern is widely used in UI-libraries, auxiliary modules and not only.

Conclusion

Of course, this is not a complete list of patterns that are used in the reactor-community. I will definitely try to tell you more if this article helps you write better, supported and concise code.