React + Reselect – Memoized Selectors for Efficient Rendering

October 24, 2017 0 Comments

React + Reselect – Memoized Selectors for Efficient Rendering

 

 

If you are familiar with React, then you probably have a general understanding of its component lifecycle. As a general rule, whenever a component’s props change, React will re-render the component.

Some of the most challenging parts of maintaining an efficient React app are keeping good control over your components’ lifecycles, and ensuring that needless re-renders are not occurring. This is easy for basic scalar types, such as a number or a string, because a shallow equality check (e.g. a === b) is sufficient to detect when a prop is changing.

But in a complex web application, there will certainly be situations where you are prop-ing down complex Object or Array types. In these cases, you need to be careful to maintain referential equality of your object, such that React can efficiently determine when to re-render your component.

Introducing Reselect

Reselect is a library which is used for creating memoized selectors. This is just a fancy way of saying that it caches the results of a function for a certain input. It is most commonly used with Redux, and the term “selector” is often used in the Redux ecosystem, referring to how one selects a portion of the Redux state.

A selector can compute any set of derived data based on any arbitrary input, memoizing the function’s result for later use. It will recompute the result whenever any of the inputs to the function change. This is very handy for two reasons:

  1. Expensive operations are only executed when they need to be, which helps with computation performance.
  2. Since the results are cached, referential equality is preserved on subsequent calls of the memoized function. Then, when the results of the memoized selector are used as props for another component, React will know it doesn’t need to re-render the sub-component.

Not Just for Redux

Looking at the README on the Reselect page, it might seem pretty clear that this library is only to be used with Redux. However, there is nothing special about Reselect that limits it to Redux. Sure, it was created to solve problems in the React-Redux ecosystem, but really, this library is useful any time props are mutated in your component chain and need to be memoized for rendering.

The examples from the README talk about mapStateToProps, which is a common pattern with the React-Redux library. In my experience, this library has been a lifesaver for many non-Redux applications on my current project.

An Reselect Example

About six months ago, I wrote a blog series about how to build a GraphQL client application using the Apollo stack. In Part 2 of the series, I shared a simple example application that would send a request to our GraphQL server to find a person by name. To demonstrate the issue with unnecessary re-rendering, I doctored up that example a bit:

 import React from 'react'; import { graphql } from 'react-apollo'; import gql from 'graphql-tag'; class Greeter extends React.Component { componentWillReceiveProps(nextProps) { console.log('this.props.person === nextProps.person: ', this.props.person === nextProps.person); } render() { const { person } = this.props; return <div>Hello {person.name} ({person.age} {person.gender})!</div>; } } Greeter.propTypes = { // comes from MyApplication desiredName: React.PropTypes.string.isRequired, someCounter: React.PropTypes.number.isRequired, // come from Apollo person: React.PropTypes.object, }; Greeter.defaultProps = { person: {} }; const PERSON_WITH_NAME_QUERY = gql` query($name: String!) { personWithName(name: $name) { gender, name, age } } `; const GreeterWithData = graphql(PERSON_WITH_NAME_QUERY, { options: ({ desiredName }) => ({ variables: { name: desiredName } }), props: ({ data: { personWithName } }) => ({ person: Object.assign({}, personWithName), }), })(Greeter); class MyApplication extends React.Component { constructor() { super(); this.state = { someCounter: 0 }; } render() { return ( <button onClick={() => this.setState({ someCounter: this.state.someCounter + 1 })}>Button</button> <GreeterWithData desiredName="George" someCounter={this.state.someCounter} /> ); } } 

It’s essentially the same code as my original example, except:

  1. The props function from the graphql component returns a new instance of person each time it is called (i.e. breaking referential equality on the person prop.
  2. There is a button in MyApplication, which updates a prop called someCounter in GreeterWithData, which, when clicked, will cause the props of GreeterWithData to change, and therefore will cause the props function in Apollo to be called.

When we run this code, we will see that the Greeter component will receive a new copy of person every time we touch the button, even though it is really supposed to be the same person–it just so happens that a new object reference is being passed down.

To fix this, we can use Reselect to build a memoized data transformation from the props function like this:

 const getName = (person) => person.name; const getAge = (person) => person.age; const getGender = (person) => person.gender; const personSelctor = createSelector( [getName, getAge, getGender], (name, age, gender) => ({ name, age, gender }) ); const GreeterWithData = graphql(PERSON_WITH_NAME_QUERY, { options: ({ desiredName }) => ({ variables: { name: desiredName } }), props: ({ data: { personWithName } }) => ({ person: personSelctor(personWithName || {}), }), })(Greeter); 

The top three functions define what data is being selected out of the object, which in this case is the person object. These functions should be very simple and quick to execute. The results are passed in as the arguments to the result selector function, which in this case is just creating an object out of each of the three results.

If any of the name, age, or gender properties change, Reselect would invoke the result selector again. Finally, from inside the props function, we invoke the personSelector, pass in the GraphQL results, and let Reselect run its magic.

If we run this code, we will see that the Greeter is not being re-rendered when the button is pressed! Since the name, age, and gender of the person object remain the same on subsequent invocations, Reselect returns its cached value, and React can easily determine that the Greeter component did not need re-rendering due to the person changing.

While this is a very simple example, I hope you can understand the power of using Reselect for memoized functions in your React application. Using these selectors whenever your props are being mutated can really help your application stay performant and efficient, even in non-Redux situations.


Tag cloud