JasonMore / rerendering-react-redux

Example app showing bad redux patterns which lead to poor performance

Home Page:rerendering-react-redux.vercel.app

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Example React Redux unnecessary re-rendering demo

Over the years redux became one of the most popular options for managing global state in react. There have been many opinions on how to structure applications that use react-redux. This example project will show how the classic redux structure leads to performance issues and how to migrate to the correct structure recommended by the redux team.

Table of Contents

Classic structure

The Classic redux structure is no longer officially documented and dead. I left the blurb below explaining what used to happen, but its impossible to visit anymore.

The (now defunct) classic redux documentation recommended structuring your project with a pattern that might look familiar if you’ve built a redux app in the last 5 years. This pattern has a containers folder that holds all your components connected to the global state, and a components folder that holds all your presentational components. If you visit this link today, you’ll the newly revamped documentation with the best practices, but if you hop into the wayback machine you’ll see a delightfully vague deprecation notice:

react deprecation

This example project will show step by step the performance and complexity downside to the classic redux structure, and how to migrate a project to use the recommended pattern according to the Redux Essentials Tutorial, which the redux team considers the most up to date documentation. They also have a best practices style guide. Combined, here are some of primary suggestions:

Best Practices

Getting State

Don’t: Create a container component whose only job it is to pass state to another component without doing any rendering or logic. This leads to unnecessary coupling and complexity, especially at scale.

Do: Connect every single component that needs a piece of state to render or accomplish an action. There is no measurable performance penalty doing this, contrary to popular belief. Quite often performance increases when components only get the small amount of state they need.

Naming

Don’t: Add the word container to a component connected to state. Example: CarContainer.

Do: Name things for what they are or do, not if they are connected to state. Example Car.

What kind of state to get

Don’t: Fetch objects and pass the object or their fields around to other components through component properties. Usually referred to as “Prop Drilling”.

Do: Fetch primitive values that will be used for rendering or logic, like strings, bools, numbers, etc. If rendering lists of things, create a hashmap of the array, then render the list but only pass the child objects an ID, which will allow a selector access to the sub property directly.

Understand how immutable objects impact performance

Redux requires your state to be immutable, so understanding how immutable works is paramount to fixing performance. Updating immutable data properly directly informs React which components need to re-render. By connecting a single component to too many pieces of data, or connecting it too high up in the tree of data, any time a leaf data element changes, the component connected to that data either directly, or to its ancestor data element will cause all of that’s components children to re-render, even though nothing actually changed.

Step 1: Original Implementation

View branch source

Here is a classic setup for any react redux app which started development 2-3 years ago. You have three directories, components, containers, and store:

src
├── App.js
├── components
│   ├── Car.js
│   └── Options.js
├── containers
│   └── CarsPageContainer.js
├── index.js
└── store
    ├── actions
    │   ├── car.js
    │   └── options.js
    ├── middleware
    ├── reducers
    │   ├── carReducer.js
    │   └── optionReducer.js
    └── store.js

example application

The <CarsPageContainer> is the only component getting data out of state, and passing it to child components

import React, { useEffect } from "react";
import { connect } from "react-redux";
import { carData } from "../_fixtures/mockCarData";
import Car from "../components/Car";
import { canToggleSelected } from "../store/actions/options";
import Options from "../components/Options";
import { addAllCars, addCar, selectCar } from "../store/actions/car";

const CarsPageContainer = ({
  carState,
  optionState,
  addAllCars,
  canToggleSelected,
  selectCar,
  addCar,
}) => {
  useEffect(() => {
    // simulate ajax load
    setTimeout(() => {
      addAllCars(carData);
    }, 500);
  }, [addAllCars]);

  return (
    <div>
      <Options
        addCar={addCar}
        canToggle={optionState.canToggle}
        canToggleSelected={canToggleSelected}
      />

      <div className="m-2 p-2">
        <h2>Cars</h2>

        <div className="container-fluid row">
          {carState.cars.map((car) => (
            <Car
              key={car.id}
              car={car}
              selectCar={selectCar}
              canToggle={optionState.canToggle}
            />
          ))}
        </div>
      </div>
    </div>
  );
};

const mapStateToProps = (state) => ({
  carState: state.car,
  optionState: state.option,
});

const mapDispatchToProps = {
  addAllCars,
  canToggleSelected,
  selectCar,
  addCar,
};

export default connect(mapStateToProps, mapDispatchToProps)(CarsPageContainer);

<Car>

import React from "react";

const Car = ({ car, canToggle, selectCar }) => {
  const onCarClicked = () => {
    if (!canToggle) return;
    selectCar(car.id, !car.selected);
  };

  return (
    <div className="card m-1" style={{ width: "18rem" }}>
      <img
        className="card-img-top"
        src={car.image}
        height={160}
        alt={car.name}
      />
      <div className="card-body">
        <h5 className="card-title">{car.name}</h5>
        <p className="card-text">
          Some quick example text to build on the card title and make up the
          bulk of the card's content.
        </p>
        <button
          className={`btn w-100 ${
            car.selected ? "btn-primary" : "btn-secondary"
          }`}
          onClick={onCarClicked}
        >
          {car.selected ? "☑︎" : "☐"} Selected
        </button>
      </div>
    </div>
  );
};

export default Car;

<Options>

import React from "react";

const Options = ({ canToggle, canToggleSelected, addCar }) => {
  return (
    <div className="m-2 p-2">
      <h2>Options</h2>
      <p>
        <button
          className={`btn ${canToggle ? "btn-primary" : "btn-secondary"}`}
          onClick={() => canToggleSelected(!canToggle)}
        >
          {canToggle ? "☑ Selection Enabled" : "☐ Selection Disabled"}
        </button>
        <button
          className="btn btn-light ml-2"
          onClick={() => addCar("astonMartin")}
        >
          Add Aston Martin
        </button>
        <button className="btn btn-light ml-2" onClick={() => addCar("audi")}>
          Add Audi
        </button>
      </p>
    </div>
  );
};

export default Options;

The carReducer has a single cars array with 36 car objects, and the optionReducer has a single canToggle boolean.

state

On load performance

In this example, mock data simulating an API is dispatched to state 500ms after the <CarsPageContainer> is mounted. In this screenshot CAR_ADD_ALL was dispatched, and subsequently the Options component was rendered again needlessly.

options renders extra

It took ~52ms for this action to update redux and render components, and 11ms for chrome to update the screen.

52ms render performance

Of the 39ms of javascript, ~24ms was components rendering

components rendering

Selected button performance

The other performance issue with this structure is every time you click any of the buttons on a car component, the entire app re-renders! The blue boxes around components show they are being re-rendered.

2020-10-16 17 36 20

This is due to the cars.map(car => …) line in the CarsPageContainer. Below is a screenshot showing all the components that rendered again but didn’t actually need to. You’ll see the Options component rendering again, along with every other car component.

cars re-rendering

It takes ~56ms to update this change, but only 2ms of that is chrome updating the screen.

cars selection

cars selection rendering

Step 2: Refactor File organization

View branch source

In this example, all the files will be restructured to match the Redux best practices suggested app structure before doing any refactoring.

src
├── App.js
├── features
│   ├── car
│   │   └── Car.js
│   ├── option
│   │   └── Options.js
│   └── pages
│       └── CarsPage.js
├── index.js
└── store
    ├── actions
    │   ├── car.js
    │   └── options.js
    ├── middleware
    ├── reducers
    │   ├── carReducer.js
    │   └── optionReducer.js
    └── store.js

Diff: https://github.com/JasonMore/rerendering-react-redux/compare/step1-naive-implementation...step2-refactor-folders

Step 3: Connecting <Options> to state

View branch source

Once the concept of a Container component is removed, and any component can get data, it no longer makes sense for <CarsPage> to send data to options it can get itself. <Options> can exist solely on its own. This reduces the complexity of <CarsPage> having to know anything about state.option.

image

image

diff: https://github.com/JasonMore/rerendering-react-redux/compare/step2-refactor-folders...step3-fix-loading-rerendering

After this change, <Options> no longer re-renders after the mock api response is set to state.

The React Profile tool also shows <Options> did not render.

The Step 1 rendering time was ~52ms, and after adding another component to state, the new rendering time is also ~52ms. Decoupling components and connecting to state provides less complex code that doesn’t impact performance.

Step 4: Connecting <Car> to state

View branch source

Clicking on any car in the list of cars causes the entire app to re-render. Fixing this requires changing the list of cars from an array to a hashmap. A hashmap is a list of objects you can reference by key. Functionally it can work the same as an array, but instead have direct access to any member of the list immediately through it’s id property. This allows direct access to any item in the list.

Diff: https://github.com/JasonMore/rerendering-react-redux/compare/step3-fix-loading-rerendering...step4-fix-selecting-car

image

State also needs to be pushed down to each <Car> by connecting it to the redux store directly. Each car can reference its own car data through the carId property passed to it. None of the properties used to render the car changed in this diff, just where the data came from.

image

Now that each <Car /> is connected to state, all of the props can be exchanged for a single id prop <Car id={car.id} />

image

After this change, only the single car re-renders on selection.

2020-10-16 18 37 43

The fix can be verified by a lack of why-did-you-render warnings after the redux action was dispatched.

By breaking up the hard connection of passing props to each car, this turns the single pass rendering into two different rendering cycles. The first render is the <CarsPage> making sure no new cars were added or removed. Since the only prop we pass to each car is only it’s id now, each car does not render again since that one property did not change. Internally the react-redux method connect checks to see if any property actually changed before allowing the component to render, exactly how PureComponent or React.memo works.

Since each component has access to state, its mapStateToProps function is called again, and if any of those change, then that component is queued up to render again. Only the single car renders again.

It originally took ~56ms to handle the button click, and these updates reduced it to ~33ms, a 40% performance improvement. This shows that connecting more components to state does not negatively impact performance, and can make it better.

Step 5: Remove connect HOC

View branch source

React and redux have come a long way in the last few years. One of the best improvements in React is the adding of hooks, which lowers the amount of cognitive load when writing react components with redux. This adds clarity to any component, as the only props to reason about are the ones actually passed into the component, not a mix of what is coming from a parent component and redux.

In the case of <Car>, the complexity is actually two fold, as the carId prop is never even used in the render method. By using react-redux hooks, the code becomes cleaner and more concise. This adds clarity to the component, as the props are actually what was passed into the component, not a mix of what is coming from a parent prop and redux.

The most important thing to remember when converting a component from connect to redux hooks, is that connect provides built in memoization of props passed to the component, just like how PureComponent and React.memo work. So if a component has legitimate need for outside props to render something, such as an id, you should consider using React.memo. In this example, converting the component without React.memo causes all the <Car> components to start re-rendering needlessly again.

Diff: https://github.com/JasonMore/rerendering-react-redux/compare/step4-fix-selecting-car...step5-remove-connect

image

image

Load and click performance

Switching from the connect HOC to functional components doesn’t change performance at all.

load:

clicking "Selected" on a car:

Step 6: Selectors

View branch source

Using selector functions for getting values out of state greatly improves the mental work of getting values out of state, allows easier refactoring of components, and is a recommended best practice by the redux team.

image

Step 6.1: Refactor store files

Keeping the store folders aligned to features, just like the components folders aligned to features colocates relevant files together.

src
├── App.js
├── features
│   ├── car
│   │   └── Car.js
│   ├── option
│   │   └── Options.js
│   └── pages
│       └── CarsPage.js
├── index.js
└── store
    ├── car
    │   ├── carActions.js
    │   ├── carReducer.js
    │   └── carSelectors.js
    ├── middleware
    ├── option
    │   ├── optionReducer.js
    │   ├── optionSelectors.js
    │   └── optionsActions.js
    └── store.js

Summary

Connecting child components to state has no measurable performance loss, and after code refactoring significant opportunity for performance gains. Connecting components with the data they actually need for rendering reduces the complexity of a component, and removes hard co-dependencies between components where they shouldn’t exist.

About

Example app showing bad redux patterns which lead to poor performance

rerendering-react-redux.vercel.app


Languages

Language:JavaScript 86.6%Language:HTML 13.4%