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.
- Example React Redux unnecessary re-rendering demo
- Table of Contents
- Classic structure
- Best Practices
- Understand how immutable objects impact performance
- Step 1: Original Implementation
- Step 2: Refactor File organization
- Step 3: Connecting
<Options>
to state - Step 4: Connecting
<Car>
to state - Step 5: Remove
connect
HOC - Step 6: Selectors
- Step 6.1: Refactor store files
- Summary
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:
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:
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.
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
.
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.
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.
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
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.
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.
It took ~52ms for this action to update redux and render components, and 11ms for chrome to update the screen.
Of the 39ms of javascript, ~24ms was components rendering
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.
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.
It takes ~56ms to update this change, but only 2ms of that is chrome updating the screen.
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
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
.
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.
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.
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.
Now that each <Car />
is connected to state, all of the props can be exchanged for a single id prop <Car id={car.id} />
After this change, only the single car re-renders on selection.
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.
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.
Switching from the connect
HOC to functional components doesn’t change performance at all.
load:
clicking "Selected" on a car:
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.
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
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.