USING RESELECT AT REACT CLASS COMPONENTS

March 27, 2019

Today at the work, I had a little trouble with refactoring our class components. The problem was our react frontend application was unresponsive and lags on user input. Too many key-downs and an input started to filling a few seconds later after user stopped typing. Horrible experience - as for programmer, as for user. I tracked the app with Chrome DevTools. Some of our components were rerendered, but the data change was not intended for these components - so they shouldn’t be rerendered right ? Rerenders happen, when store changes - and store changes when we update input (and we have a lot of inputs).

Our main view looks something like this:

class Panel exntends PureComponent {
   
   compute = (item, counter, anotherCounter) => {
      ... heavyFunction ...
   }

   render() {
      
      const counter1 = {}, counter2 = {};
      
      for (let i = ...) {
         ... a lot of computing
      }

      return (
         <div>
            <HeavySection 
               onCreate={item => this.compute(item, counter1, counter2)}
            />
         </div>
      );
   }
}

On line 18 we passed anonymous function to the component HeavySection …well, is this a problem? In most cases not. In my case it was a problem because rendering of the component last a few hundred milliseconds. The component Panel was connected to the redux store. The whole app view is heavy occupied with controlled inputs subscribed to the store (onChange). When a value in the inputs has changed, the store will update with the new immutable value. So the extending from PureComponent however, doesn’t help as soon as reference check fail at the store variable (connected to the component). There is a minimal chance of optimizing the parent panel. The next problem however, is that in render method we have HeavySection component with rerender time in a few hundred milliseconds. For the simplicity, we have only one HeavyComponent, but we could end up with 10-50 with HeavyComponents. Let’s say, the HeavyComponent is extended from PureComponent as well. Again, it doesn’t help as soon as it receives the prop onCreate with new anonymous function - every rerender creates new anonymous function again - therefore new reference is created and shallow check at PureComponent (shouldComponentUpdate) will fail!

What can we do with that ?

There is 95% chance you can extract that anonymous function and move to the arrow method like this:

Before:

class Component ext... {
   render() {
      return (
         <Heavy onClick={(arg) => this.setState({...})} />
      )
   }
}

After

class Component ext... {
   onClickHandler = (arg) => this.setState({...})
   
   render() {
      return (
         <Heavy onClick={this.onClickHandler} />
      )
   }
}

Cool! Now the reference of the method is always rock static and will not change, therefore your Heavy component will not be rerendered. Job done.

What about our case, remember ?

class Panel exntends PureComponent {
   
   compute = (item, counter, anotherCounter) => {
      ... heavyFunction
   }

   render() {
      
      const counter1 = {}, counter2 = {};
      
      for (let i = ...) {
         ... a lot of computing
         ... setting counters ...
      }

      return (
         <div>
            <HeavySection 
               onCreate={item => this.compute(item, counter1, counter2)}
            />
         </div>
      );
   }
}

Do you remember how I told you 95% of anonymous functions in a prop fields are rewritable ? Unfortunatelly, this is the case of these last 5%.

You see that our HeavySection executing onCreate function with one argument item. This function calls another function this.compute with arguments item, counter1, counter2. So we can’t extract the function to the method, because this called function depends on the variables computed inside render method. For the sake of simplicity and saved time - I will tell you that I am not merely happy with “heavy computiation” inside a render method - you always should be prepared and cautious when something like this start to happen. Render method should always do, what it is called for - to Render! In a best optimized world, we would move the computation outside of the render method and outside of the component - to the store actions or reducer, or in the component method once a time and then save values in the component state. But now, the computing mechanism inside the render method is not readable and uses a lot of props on the computation. So we’ve got a little spaghetti here and it would not be efficient spending a few hours of removing bad looking and unreadable code outside of the render method.

Our purpose was to fasten the application and get rid of eventual lags at typing. We solve that by passing not-changing values to the props of our components.

For clarification see why we can’t extract to the method:

Before

class Panel exntends PureComponent {
   
   compute = (item, counter, anotherCounter) => {
      ... heavyFunction
   }

   render() {
      
      const counter1 = {}, counter2 = {};
      
      for (let i = ...) {
         ... a lot of computing
         ... setting counters ...
      }

      return (
         <div>
            <HeavySection 
               onCreate={item => this.compute(item, counter1, counter2)}
            />
         </div>
      );
   }
}

 After?

class Panel exntends PureComponent {
   
   compute = (item, counter, anotherCounter) => {
      ... heavyFunction
   }

   // We don't have counter1 and counter2 variables because they are computed inside render method
   extractFunction = (item) => this.compute(item, counter1 ???, counter2 ???)

   render() {
      
      const counter1 = {}, counter2 = {};
      
      for (let i = ...) {
         ... a lot of computing
         ... setting counters ...
      }

      return (
         <div>
            <HeavySection 
               onCreate={this.extractFunction}
            />
         </div>
      );
   }
}

As you can see, we can’t do that. We need the counter1 and counter2 variable coming from render method.

Solution to the problem

Have you heard about reselect package? I had this npm package in my mind, heard about it but never used it on the frequently basis. Never had to.

You can find functions as createSelector, defaultMemoize,createSelectorCreator and createStructureCreator. For more information see link.

DefaultMemoize solves this problem. I will explain you how. Here you can see the definition of the defaultMemoize function.

function defaultMemoize(func, equalityCheck = defaultEqualityCheck)

At first, I was a little bit confused how I have to use this function. Both arguments func and equalityCheck are functions. Second function is optional and if you don’t provide the function defaultEqualityCheck will be placed as default equalityCheck function. defaultEqualityCheck function looks as simple as:

function defaultEqualityCheck(currentValue, previousValue) {
   return currentValue === previousValue;
}

Then you call defaultMemoize like this.

const compute = defaultMemoize(fn, comparator);

When you call defaultMemoize, it returns you another function. When you call this returned function, it decides whether return cached value or compute new value. How? By comparator function passed as second argument to the defaultMemoize. By calling the returned function from defaultMemoize, it always returns you a value - cached or computed - faster or slower. And how the value computed is based on fn function (first argument of the defaultMemoize function). Comparator function is called whenever you call compute function (returned by defaultMemoize) - if comparator returns true - cache value will be immediately returned. If it returns false, it recomputes the first argument function fn again.

Example

const obj = {
   arr: [1,2,3,4,5,6,7],
   hash: "efdk3j12b",
};

const computeSum = defaultMemoize(
   obj => obj.reduce((acc, val) => acc + val, 0),
   (current, previous) => current.hash === previous.hash,
);

// expensive first time computation
const firstSum = computeSum(obj);

// see, it has total same values as obj
const newSameObj = {
   arr: [1,2,3,4,5,6,7],
   hash: "efdk3j12b",
};

// compute second time -> cheap, 
// comparator will be true, returned cache
const secondSum = computeSum(newSameObj);

firstSum === secondSum // true

Notes: In this example is object obj which is immutable and for some reason he always recreates - it’s values are same but the reference is always changing. It has arr attribute and hash attribute. We know, that hash is the hash of the array and we want to recompute the sum only if the hash has changed. The memoize function describes how it is done.

Solve anonymous function problem

Before

class Panel exntends PureComponent {
   
   compute = (item, counter, anotherCounter) => {
      ... heavyFunction
   }

   render() {
      
      const counter1 = {}, counter2 = {};
      
      for (let i = ...) {
         ... a lot of computing
         ... setting counters ...
      }

      return (
         <div>
            <HeavySection 
               onCreate={item => this.compute(item, counter1, counter2)}
            />
         </div>
      );
   }
}

After

class Panel exntends PureComponent {
   
   compute = (item, counter, anotherCounter) => {
      ... heavyFunction
   }

   createHandlerMemo = defaultMemoize(
      obj => item => this.compute(item, obj.counter1, obj.counter2),
      (current, prev) => 
         _.isEqual(current.counter1, prev.counter1) &&
         _.isEqual(current.counter2, prev.counter2),
   )

   render() {
      
      const counter1 = {}, counter2 = {};
      
      for (let i = ...) {
         ... a lot of computing
         ... setting counters ...
      }

      return (
         <div>
            <HeavySection 
               onCreate={this.createHandlerMemo({counter1, counter2})}
            />
         </div>
      );
   }
}

We created memoizedSelector inside component as the method. As the first argument (selector) says, it computes out function which will be passed to the component HeavySection. In the comparator, we compare both counters, if they are unchanged - then return previous cached function with the same reference! The HeavySection will not be rendered again and our app is going to be fast!

I have done this today at work - replaced some anonymous functions with memoized selectors and got our app to run significantly faster.

As you may know, the new version of the React uses hooks and one of the hooks is useMemo. Unfortunatelly, you can use useMemo only in functional components. However, you can use defaultMemoize inside your Class Components. Yay!

Thanks for reading!

Appendix

I described optimization of the component rerendering on every change of “store”. In our app, due to the age of the project and old technologies used, we had two stores in our app - old store+dispatcher and redux store. We gradually and slowly rewriting functionality to the new redux store and planning to get rid of old store+dispatcher thing. This old store is somehow injecting the values to the components through router and routes. We injecting the store values to the route components by cloning these routes (React.cloneChildren) and passing the new props into them.

This is the reason why I didn’t talk about connect function at all which is part of the react-redux npm package. This is the high-order component injecting store state into wrapped component. The advantage is I can specify which component gets the store and the second advantage is you can choose which state variables to inject into component. And also third one - you can make optimization on the connect level and prevent also rerendering of wrapped component - which was not unfortunatelly our case at the work - because we didn’t have any other option, but to work with the old approach of injecting props into component.

If you are using redux, connect and mapStateToProps - I encourage you to use reselect mostly in your mapStateToProps function.

Premature optimization is the root of all evil. Donald Knuth

If you are in hurry and your wrapped component is not big - I understand you and therefore recommend to omit the reselect approach in this case and instead choose simple approach like pictured below. If your rerender doesn’t slow your app - it doesn’t matter for now and you can easily refactor in the future.

const mapStateToProps = state => ({
   foo: state.foo
})

export default connect(mapStateToProps)(ChildComponent);

Join the Newsletter

Name
E-Mail