- Notifications
You must be signed in to change notification settings - Fork0
Example app showing bad redux patterns which lead to poor performance
JasonMore/rerendering-react-redux
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
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 usereact-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
connectHOC - 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 acontainers folder that holds all your components connected to the global state, and acomponents folder that holds all your presentational components. If you visit thislink today, you’ll the newly revamped documentation with the best practices, but if youhop 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 theRedux Essentials Tutorial, which the redux team considers the most up to date documentation. They also havea 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 wordcontainer to a component connected to state. Example:CarContainer.
Do: Name things for what they are or do, not if they are connected to state. ExampleCar.
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, sounderstanding 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.jsThe<CarsPageContainer> is the only component getting data out of state, and passing it to child components
importReact,{useEffect}from"react";import{connect}from"react-redux";import{carData}from"../_fixtures/mockCarData";importCarfrom"../components/Car";import{canToggleSelected}from"../store/actions/options";importOptionsfrom"../components/Options";import{addAllCars,addCar,selectCar}from"../store/actions/car";constCarsPageContainer=({ carState, optionState, addAllCars, canToggleSelected, selectCar, addCar,})=>{useEffect(()=>{// simulate ajax loadsetTimeout(()=>{addAllCars(carData);},500);},[addAllCars]);return(<div><OptionsaddCar={addCar}canToggle={optionState.canToggle}canToggleSelected={canToggleSelected}/><divclassName="m-2 p-2"><h2>Cars</h2><divclassName="container-fluid row">{carState.cars.map((car)=>(<Carkey={car.id}car={car}selectCar={selectCar}canToggle={optionState.canToggle}/>))}</div></div></div>);};constmapStateToProps=(state)=>({carState:state.car,optionState:state.option,});constmapDispatchToProps={ addAllCars, canToggleSelected, selectCar, addCar,};exportdefaultconnect(mapStateToProps,mapDispatchToProps)(CarsPageContainer);
<Car>
importReactfrom"react";constCar=({ car, canToggle, selectCar})=>{constonCarClicked=()=>{if(!canToggle)return;selectCar(car.id,!car.selected);};return(<divclassName="card m-1"style={{width:"18rem"}}><imgclassName="card-img-top"src={car.image}height={160}alt={car.name}/><divclassName="card-body"><h5className="card-title">{car.name}</h5><pclassName="card-text"> Some quick example text to build on the card title and make up the bulk of the card's content.</p><buttonclassName={`btn w-100${car.selected ?"btn-primary" :"btn-secondary"}`}onClick={onCarClicked}>{car.selected ?"☑︎" :"☐"} Selected</button></div></div>);};exportdefaultCar;
<Options>
importReactfrom"react";constOptions=({ canToggle, canToggleSelected, addCar})=>{return(<divclassName="m-2 p-2"><h2>Options</h2><p><buttonclassName={`btn${canToggle ?"btn-primary" :"btn-secondary"}`}onClick={()=>canToggleSelected(!canToggle)}>{canToggle ?"☑ Selection Enabled" :"☐ Selection Disabled"}</button><buttonclassName="btn btn-light ml-2"onClick={()=>addCar("astonMartin")}> Add Aston Martin</button><buttonclassName="btn btn-light ml-2"onClick={()=>addCar("audi")}> Add Audi</button></p></div>);};exportdefaultOptions;
ThecarReducer has a singlecars array with 36car objects, and theoptionReducer has a singlecanToggle boolean.
In this example, mock data simulating an API is dispatched to state 500ms after the<CarsPageContainer> is mounted. In this screenshotCAR_ADD_ALL was dispatched, and subsequently theOptions 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 thecars.map(car => …) line in theCarsPageContainer. Below is a screenshot showing all the components that rendered again but didn’t actually need to. You’ll see theOptions 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 matchthe 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.jsOnce the concept of aContainer 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 aboutstate.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.
TheStep 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 providesless 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 thecarId 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 ofwhy-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’sid now, each car does not render again since that one property did not change. Internally thereact-redux methodconnect checks to see if any property actually changed before allowing the component to render, exactly howPureComponent orReact.memo works.
Since each component has access to state, itsmapStateToProps 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 thecarId 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 fromconnect to redux hooks, is thatconnect provides built in memoization of props passed to the component, just like howPureComponent andReact.memo work. So if a component has legitimate need for outside props to render something, such as anid, you should consider usingReact.memo. In this example, converting the component withoutReact.memo causes all the<Car> components to start re-rendering needlessly again.
Switching from theconnect 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 isa recommended best practice by the redux team.
Keeping thestore 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.jsConnecting 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
Resources
Uh oh!
There was an error while loading.Please reload this page.


























