How to not be deceived by Redux

Over the past year I have come across a number of engineers who appear to have some misunderstandings about how certain aspects of React communicate with Redux.

I’d like to clarify two of these aspects which are the most frustrating for me, as follows:

  1. Storing state in a component or in a Redux store
  2. Preventing unnecessary component rendering

Let’s review both in more details.

Storing state in a component or in a Redux store

There are a lot of articles and courses on this subject explaining that Redux is supposed to store a state, and React is supposed to render a component. However, I can still see smart components which manage their state themselves – this is not ok, although, it’s also not that big deal. What is really shocking, is that engineers store the same state both in a Redux store and a component! As a result, engineers need to synchronize the internal state with the external one using the componentWillReceiveProps. This is not a joke – it’s the reality!

function componentWillReceiveProps(nextProps) {
   this.setState({
      props1: nextProps.props1
   })
}

This demonstrates that many people still can’t decide where to store a state. I believe this is because they are more familiar with React, and can’t imagine that Redux can replace the internal component state. So they choose to work with both which is a big mistake. In the modern world, the React component is a just a view without its state, and state duplication is moveton (bad manners). We’ll have two places to store the same data, and we’ll have to reconcile them each time. This approach has only drawbacks as we will have no “source of truth” and each time we will have to decide where is “true” and write tons of code to synchronize them.

We do acknowledge that sometimes it’s more convenient to store a state in a component, and in certain situations it’s acceptable. We are well aware of the fact that Redux has a weak point which is the difficulty of components reusability. From this perspective, it makes sense to have its own component state. In any other cases, the necessity of it is quite questionable.

I’d recommend storing all data in Redux if you have no intention to reuse components. With this approach, your components is just a view. Even if you decide to make your components smarter and add an internal state, you can always create high order components over these view components and place state into them.

Preventing unnecessary component rendering

We can control our rendering by shouldComponentUpdate method – it is our silver bullet, and we can thank Facebook for it. But in a well-designed application, this method should not be needed. Redux / React communication can be set up in such a way that unnecessary page / components rendering can be avoided without using shouldComponentUpdate. In my own experience, I don’t remember the last time I used shouldComponentUpdate, and all my components work perfectly since Redux is really convenient to use.
However, I have seen the other side of the combined work of Redux / React when a component rerenders with every store change, even when it’s not necessary. For me, this is the biggest mistake all React / Redux engineers make – not being careful with performance. Yes, it works, but it is an indication that users do not understand one of the essential things about Redux – rerender only if something changes.

After each Action every component connected to Redux executes mapStateToProps and if it returns new Props, then the connected component will be rendered. Therefore, we have to be very careful with mapStateToProps returned values.

Do not connect all models – connect only a leaf of store tree.

Common example: model: { name, age, email, … } – and we take the model from store and then put it as props to component.
Store (model) -> (model) Component

Looks convenient – one model – one component. But in reality when we change the name, we change the whole model and should render the whole component and all fields (age, email, etc.) even we don’t change them. Just imagine, if your model has not just 2-3 fields but 100, 500 or more, there be a significant performance impact.

In the example above you should have one container with a model and connect each field (name, age, email, etc.) to a store separately. Maybe it doesn’t look so cool, but this is the right approach.

Let’s take an example with 1000 cells without any optimization like debouncer etc. In the initial implementation we connect our model from store (with 1000 cells) to view as a solid object (full model with all data) and then our component draw all cells and handle changes to each cell:

Initial solution Link to Codepen, 1000 cells.

// Wrong example, very bad performance
 
const TextInput = ({ model, fieldName, changeField }) => {
  console.log('render', fieldName);
  return <input
             value={model[fieldName]}
             onChange={e => changeField(fieldName, e.target.value)} />
};
const FormMapStateToProps = state => ({ model: state.model })
const FormView = ({ model, changeField }) => (
  <div>
    <div>
      {
        Array.apply(null, {length: CELLS_COUNT})
          .map((_, index) =>
               <TextInput
                  key={index}
                  model={model}
                  changeField={changeField}
                  fieldName={`cell${index}`} />)
      }
    </div>
  </div>
);
const Form = connect(
  state => ({ model: state.model }),
  { changeField: (fieldName, value) =>
     ({ type: `CHANGE_${fieldName}`, payload: value}) }
)(FormView);

Now let us do a change and connect each cell to a store separately. It is not obvious and could look strange, but it works 10 times faster and without additional optimization.

Better solution: Link to Codepen the same 1000 cells work 10 times faster.

const TextInputView = ({ value, fieldName, changeField }) => {
  console.log('render', fieldName);
  return <input
     value={value}
     onChange={e => changeField(fieldName, e.target.value)} />
};
const TextInput = connect(
  (state, ownProps) => ({ value: state.model[ownProps.fieldName] }),
  { changeField: (fieldName, value) =>
     ({ type: `CHANGE_${fieldName}`, payload: value}) }
)(TextInputView);
 
const FormView = () => (
  <div>
    <div>
      {
        Array.apply(null, {length: CELLS_COUNT})
          .map((_, index) =>
               <TextInput key={index} fieldName={`cell${index}`} />)
      }
    </div>
  </div>
);

Of course, you can optimize the initial example by replacing function component to PureComponent. However, if we work with real data our field is not a simple value, but an object with { value, initialValue, validators, pristine, errors, type, etc. } so you can’t do this optimization, you’ll have to write a function shouldComponentUpdate and decide whether to update or not by comparing a lot of fields and each time you need to remember to update this function with changing field structure. But, as we discussed earlier, Redux can do it for us ‘from a box’.

Create the simplest mapStateToProps possible – allow Redux to decide whether the props changed or not.

Another “good” approach is to convert values from store into a new object. Common example: is to filter values in the list, e.g. mapStateToProps = state => ({ list: state.list.filter(item => …) });

As a result after each Action we receive a new object. Even if it was Action in an absolutely different part of an application, the component connected by this means will be rendered because it always receives a new list.

Another example:

// component1.jsx
// if we connect Component1 in this way, it will be rendered on every store
// change even if it is not related to Component1,
// eg. if we change data in Component2
mapStateToProps = state => (
   { model: { prop1: state.prop1, prop2: state.prop2 } }
);
// component2.jsx
mapStateToProps = state => ({
   prop3: state.prop3
});
// main.jsx
const Main = () => (
   <div>
      <Component1 />
      <Component2 />
   </div>
)

Every time this function will return a new object and Redux won’t be able to prevent rendering. So the right solution for the former example is to use reselect library. If you use Redux, you have to be familiar with it. For the latter example, it is enough just to pass props separately (or use reselect).

// using reselect library
// in this example even if change prop3 in Component2,
// Component1 won't be re-rendered and it's expected behaviour
// without reselect it would be rerender even we do not change prop1 or prop2
 
import { createSelector } from 'reselect';
 
// create simple selectors
const selectProp1 = state => state.prop1;
const selectProp2 = state => state.prop2;
 
// memoized selector which returns new value only if changed props1 or props2
const selectModelWithProps1And2 = createSelector(
   selectProp1, selectProp2,
   (prop1, prop2) => ({ model: { prop1, prop2 } })
);
 
// component1.jsx
// in this way (with reselect),
// Component1 will be rendered only if changes relating to it
mapStateToProps = state => {
   const { model } = selectModelWithProps1And2(state);
   return { model }
};

Reselect is good but we should understand what it can and cannot do

Reselect is just a memorization tool and there isn’t any magic inside. I saw a selector like this:

const simpleValue = state => state.value;
 
const simpleSelector = createSelector(
   simpleValue,
   value => value
);

There is no point in using these types of selectors. You can just write : simpleSelector = simpleValue;
Using Reselect makes sense only if the results depend on two or more of its parameters (ideally stored in store). However, if you store the list of values and a filter for this list in store and want a selector to return the filtered list, Reselect is highly recommended.

If you connect your view component to store as in the following example:

mapStateToProps = state => {
   filteredList: state.list.filter(item => filterFunc(item, state.filter))
}

mapStateToProps function will return a new filtered array each time even if the Event is not related to this component and the list of values and the filter haven’t been changed, which will result in component rerendering.

This situation is ridiculous and Reselect was created to prevent it. And you should use it to write the following:

const selectList = state => state.list;
const selectFilter = state => state.filter;
 
const selectFilteredList = createSelector(
   selectList, selectFilter,
   (list, filter) => list.filter(item => filterFunc(item, filter))
);
 
mapStateToProps = state => {
   filteredList: selectFilteredList(state)
}

In this case, we will avoid all unnecessary rerendering. Reselect stores the result and returns the new one only if the list or the filter has been changed.

To sum up

React combined with Redux gives us modern Model -> View solution. Redux is a Model and React is a view. It sounds good when you read about it, but we live in a real world and we have to care about performance.

Here is a checklist to follow how to do it:

  1. Use React just for the view and send a simple action to Redux. You don’t need any business logic here. However, please remember, there is no silver bullet, and sometimes a small solid component with its own state work perfectly fine.
  2. Connect Redux to React very carefully. Component must receive only the related changes and skip changes on the unrelated props.
  3. Use Reselect for calculations to avoid returning of new props in the connected components.
  4. Remember to turn on “Highlight Updates” in React dev tool regularly and control what you are doing.

My message is: React and Redux can make you powerful or powerless – you decide what to choose!