0% found this document useful (0 votes)
130 views27 pages

How We Reduced Our Re-Renders by 80% With React Redux and Custom Context - Console - Blog

How we reduced our re-renders by 80% with React Redux and custom context - console.blog

Uploaded by

ARANIABD
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
130 views27 pages

How We Reduced Our Re-Renders by 80% With React Redux and Custom Context - Console - Blog

How we reduced our re-renders by 80% with React Redux and custom context - console.blog

Uploaded by

ARANIABD
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 27

2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.

blog

How we reduced our re-renders by


80% with React Redux and custom
context
2021-01-29

Introduction
In the project I’m currently working on for a client (a global denim brand), my collegues
and I started to notice performance degradation in our React app, which mostly consists
of complex forms and different kinds of input components. Specifically, we’re working on
the webshop checkout, a pretty crucial part in any webshop. Performance started to get
so bad after a while that basic input fields couldn’t keep up with fast typing. This was
annoying at first, but soon required action.

When we started looking into the cause of these performance issues, we found that most
React components in the form would re-render whenever any form input value changed.
We knew that this was caused by poor (un-optimized) handling of user events and data
flow. Some of our components were hundreds of lines of JavaScript long - a red flag on
it’s own - and had dozens of props that were being passed down multiple levels deep.
This can impact performance as well, and debugging such components is no fun either.
This is also often referred to as ‘prop-drilling’.

Digging a little deeper, we looked into our JavaScript performance using Chrome’s JS
Profiler. Here we noticed that some frames would take up to 250-1000ms to render,
when ideally you’d want that to be under 16.6ms for your browser to be able to render
the app at 60fps.

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 1/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

When we measured how many times our Input component would render during a
normal user interaction flow, we saw that in some cases the component would re-render
up to 800-1300 times, whoops!

Identifying the problem

We were using React Context (https://reactjs.org/docs/context.html) for a lot of things -


for retrieving values, accessing setter / getter functions and to keep track of the entire
form state. While I do love the introduction of React Context and it’s ease of use, I
suspected that - out of the box - things weren’t as optimized as one might assume. This
started to become a problem as our app grew in complexity.

This might seem obvious, but if you notice that your machine is struggling with the app
you’re building, it’s safe to assume that the experience for users on lower-end devices is
going to be horrible. This especially becomes a problem when you’re working on a multi-
language, globally used web app for example. As developers we’re usually working on
fast Macbook Pro’s (or other up-to-date hardware), I’m using a 6-Core Intel Core i7 with
16GB of RAM for reference. It’s good to keep in mind, because performance in React
apps doesn’t seem to become a problem that much these days.

I’ve worked with Redux (https://redux.js.org/) and React Redux (https://react-


redux.js.org/) a lot in previous projects, and I know that while it does add some
boilerplate and a few kb’s to your bundle, in my experience it’s always proven itself to be
very reliable - and has matured nicely over the years 👌. Smart people have worked long
and hard to optimize it’s performance so I feel comfortable using it in “mission-critical”
situations.

Let’s dive into the approach we took to optimize the performance of our React
application!

Our performance optimization todo’s

Before we started, we made a summary of things that we knew would improve overall
performance. Not all of these improvements (such as using React.memo ) are covered in
this post, but as you’ll see I’ve tried to cover the most important parts.

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 2/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

1. Move business logic to container components and render presentational


components
2. Use React.useCallback for functions that are passed down to children
3. Use React.memo for components that receive many props but don’t need to re-
render
4. Use React.useMemo to cache computationally heavy functions
5. Move a lot of re-used business logic to hooks
6. Use reselect (https://github.com/reduxjs/reselect) to memoize and compose
Redux selectors
7. Use a Redux store with custom context instead of a ‘plain’ React Context
8. Prevent re-renders by avoiding prop-drilling as much as possible

TL;DR - Results

We’ve measured a typical user flow / journey through the React checkout application,
filling in a few forms, etc.

70-85% less component re-renders on average


80-97% less blocking time on average (in Profiler, as illustrated below)
~8% better Speed Index (in Lighthouse)

Improvements from a developers perspective:

Seperation of concerns by using custom hooks and container / presentational


components
Cleaner and more maintainable codebase
We discovered and fixed a handful of previously undetected bugs
Interaction with checkout forms feels noticeably faster

Chrome’s JavaScript Profiler visualises this nicely. These screenshots show the before
and after of a typical user flow, filling in the form in 20-30 seconds. Additionally, it shows
the TBT (Total Blocking Time). TBT measures the total amount of time that a page is
blocked from responding to user input, such as mouse clicks, screen taps, or keyboard
presses (https://web.dev/lighthouse-total-blocking-time/).

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 3/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

TBT before: 12901ms (ouch)


TBT after: 388ms 😺

So, where do we start?

Your app currently looks something like this

You’ve got a React app with an existing global store (this is optional). I’ve included a
global Redux store just to illustrate that we can use multiple stores next to each other.
There’s a good chance that you, or your company or client uses Redux already anyway.

Here we’ll focus mainly on the components rendered inside <App /> , I don’t really care
about the rest of the app, this might have been written by someone else, so preferably I
don’t even need to touch it.

index.jsx

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 4/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

import React from "react"


import { render } from "react-dom"
import { Provider } from "react-redux"

import store from "./globalStore"

import App from "./App"

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root"),
)

Click this button to hide all JavaScript comments

With a ‘global’ Redux store already in place

Quick example of a store that might exist inside your app already.

globalStore.js

import { createStore } from "redux"

/**
* Define and export action types.
*/
export const INIT_APP = "INIT_APP"
export const SOME_GLOBAL_UPDATE_FORM_ACTION = "SOME_GLOBAL_UPDATE_FORM_ACTION"
export const SOME_GLOBAL_RESET_FORM_ACTION = "SOME_GLOBAL_RESET_FORM_ACTION"

/**
* Example 'global' Redux store
* that might exist in your app already.
*/
const initialState = {
initialized: false,
forms: {},
someUserData: {},
// ... etc
}

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 5/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

/**
* A standard reducer function.
*
* @param {object} state
* @param {object} action
* @returns {object} nextState
*/
const reducer = (state = initialState, { type, payload }) => {
switch (type) {
case INIT_APP:
return {
...state,
initialized: true,
}
case SOME_GLOBAL_UPDATE_FORM_ACTION:
return {
...state,
forms: { ...state.forms, [payload.formId]: payload.values },
}
case SOME_GLOBAL_RESET_FORM_ACTION:
return {
...state,
forms: Object.fromEntries(
Object.entries(state.forms).filter(([key]) => key !== payload.formId),
),
}
// ... etc
default:
return state
}
}

export default createStore(reducer)

But now we’ve got several instances of large /


complex components in our app

In case of the project I’m working on we’re dealing with ‘dynamic’ forms which can be
configured in our CMS. These forms contain a lot of logic (too much IMHO), but these
are real-world scenarios we must deal with in larger codebases.

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 6/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

Here you can see that we render the App component with some children, and when the
app renders for the first time it dispatches an example action INIT_APP to the ‘global’
Redux store. This is just to illustrate what your current app might look like.

Imagine that the three FormContainer components contain lots of logic and different
child components, their configuration is determined dynamically using the formId value
for example.

App.jsx

import React, { useEffect } from "react"


import { useDispatch, useSelector } from "react-redux"

import { FormContainer } from "./FormContainer"

/**
* App component with some example form components.
*
* @returns {React.FC}
*/
const App = () => {
const dispatch = useDispatch()
const isInitialized = useSelector((state) => state.initialized)

useEffect(() => {
if (!isInitialized) dispatch({ type: "INIT_APP" })
}, [dispatch, isInitialized])

return (
<div className="App">
<h1>React + Redux app</h1>
<hr />
<FormContainer formId="homeBillingForm" />
<FormContainer formId="homeShippingForm" />
<FormContainer formId="guestLoginForm" />
</div>
)
}

export default App

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 7/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

Let’s seperate logic with container and


presentational components

Presentational components can be tested with ease. Simply mock the required props
and you’re good to go.

You should be able to write these components without using the return keyword, as in
the following examples. I personally try to do no logic at all inside presentational
components.

Form.jsx

import React from "react"

/**
* Form presentational component.
*
* @returns {React.FC}
*/
const Form = ({ children, ...props }) => <form {...props}>{children}</form>

export default Form

FormInput.jsx

import React from "react"

/**
* Example input presentational component.
*
* @returns {React.FC}
*/
const FormInput = ({ children, ...props }) => (
<div className="FormInput">
<input type="text" {...props} />
{children}
</div>
)

export default FormInput

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 8/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

And we’ll use a React Context


(https://reactjs.org/docs/context.html)

Simply create an empty context, we will use this later on.

context.js

import { createContext } from "react"

const FormContext = createContext(null)

export default FormContext

Now we’ll create the container components

This is where things start to get interesting.

The FormContainer component is just a regular component where the business logic of
the form is defined. This is the job of a container component. It uses a regular
useDispatch to dispatch actions to the ‘global’ store, and it uses some custom hooks
(https://reactjs.org/docs/hooks-custom.html) that we’ll define later.

FormContainer.jsx

import React, { useCallback, useEffect, useRef } from "react"


import { Provider, useDispatch } from "react-redux"

import FormContext from "./context"

import { SOME_GLOBAL_UPDATE_FORM_ACTION } from "./globalStore"


import { createFormStore, useFormActions, useFormSelector } from "./hooks"
import { valuesSelector, requiredFieldsFilledSelector } from "./selectors"

import Form from "./Form"

/**
* Form container component.
* In this component we will handle business logic.
*
* @returns {React.FC}
*/
t F C t i ({ f Id hild }) {
https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 9/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog
const FormContainer = ({ formId, children, ...props }) => {
/**
* We're still free to use the 'global' dispatch function wherever we like.
*/
const dispatch = useDispatch()

/**
* Get a function from a custom hook which we'll write later on
*/
const { setFormId } = useFormActions()

/**
* Get values from the form store using our
* custom Redux hook 'useFormSelector'.
*/
const values = useFormSelector(valuesSelector)
const requiredFieldsFilled = useFormSelector(requiredFieldsFilledSelector)

useEffect(() => {
setFormId(formId)
}, [setFormId, formId])

const onSubmitHandler = useCallback(


(e) => {
e.preventDefault()

if (requiredFieldsFilled) {
dispatch({
type: SOME_GLOBAL_UPDATE_FORM_ACTION,
payload: { formId, values },
})
}
},
[dispatch, requiredFieldsFilled, formId, values],

return (
<Form onSubmit={onSubmitHandler} {...props}>
{children}
</Form>
)
}

/**
* Connected form container component.
* Connects the component to the form context.
* Thi ll th f t R d h k
https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 10/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog
* This allow the use of custom Redux hooks.
* https://react-redux.js.org/next/api/hooks#custom-context
*/
const ConnectedFormContainer = connect(null, null, null, {
context: FormContext,
})(FormContainer)

/**
* Form container provider.
* Provides a Redux store for each form instance and renders the connected form conta
* https://react-redux.js.org/using-react-redux/accessing-store#providing-custom-cont
*
* You can provide an optional parameter 'store' which can be useful when writing tes
* (not covered in this blog post)
*
* @returns {React.FC}
*/
const FormContainerProvider = ({ store, children, ...props }) => {
/**
* Create a new store for every instance of FormContainerProvider.
* 'useMemo' makes sure we create the store only once before the component will mou
* 'useRef' makes sure we get a consistent reference to the store object.
*/
const formStore = useMemo(() => store || createFormStore(), [])
const { current } = useRef(formStore)

return (
<Provider context={FormContext} store={current}>
<ConnectedFormContainer {...props}>{children}</ConnectedFormContainer>
</Provider>
)
}

/**

* Export the form container provider instead of the form container component.
*/
export { FormContainerProvider as default }

Use the connect HOC (higher-order component) function to specify which context
the FormContainer component needs to connect to.
Pass the custom context and a Redux store as props to a default Redux Provider
and wrap the connected component to create a FormContainerProvider that
provides the current store to any child components.
https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 11/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

Now a FormInput component can reliably retrieve values from the store of the form it’s
currently rendered in, even though the component itself is used across different forms.
🤯

I wasn’t aware that you could pass in an options object as the fourth argument to the
connect function! We can simply use null for the first three arguments because we
don’t need to use mapStateToProps , mapDispatchToProps or mergeProps . We want to
get values using Redux selector hooks instead of mapping state variables to component
props. This was common practice when we were working with class-based components,
but nowadays we’d like to work with functions (hooks) only.

Now when we render three different instances of FormContainer for example, three
Redux stores will be created, each with the same initial state but unique internal state
over time.

In theory you could pass some initial state as props to the FormContainer component,
which you would pass into the createFormStore function if you want to set up the store
with initial values based on props.

Getting form values and firing actions from a deeply


nested component

It doesn’t matter how deep this component is nested inside the FormContainer .
Calling setValue will update a value in the current form store only, as described above.
No more hassle with mapStateToProps or mapDispatchToProps ! 😁

FormInputContainer.jsx

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 12/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

import React from "react"

import { useFormActions } from "./hooks"

import FormInput from "./FormInput"

/**
* Form input container component.
*
* @returns {React.FC}
*/
const FormInputContainer = ({ name, children, ...props }) => {
/**
* ❌ creates a subscription and triggers a re-render on all updates to FormContex
*/
// const { someValue } = useContext(FormContext)

/**
* ⚠ creates a subscription to a single store value.
https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 13/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

*/
// const someValue = useFormSelector(state => state.someValue)

/**
* ✅ creates a memoized subscription to a single store value.
*/
// const someValue = useFormSelector(someValueSelector)

const { setValue } = useFormActions()

const onChangeHandler = useCallback((e) => setValue(name, e.target.value), [


setValue,
name,
])

return (
<FormInput onChange={onChangeHandler} {...props}>
{children}
</FormInput>
)
}

export default FormInputContainer

Setting up some example complex logic for our form

We would need to validate input, write things to localStorage - but not when it’s a
password - etc.

utils.js

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 14/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

/**
* Just a single example of all the utility functions
* that we would use in our complex form.
*
* @param {string} value An input value
* @param {object} validationRule A dynamic validation rule
* @returns {boolean} isValid
*/
export const validateInput = (value, validationRule) => {
if ((value === undefined || value === "") && validationRule?.isRequired) {
return false
}

if (value && value?.length > validationRule?.maxLength) {


return false
}

return true
}

And this is where the magic happens

Hooks.

React Redux exposes three useful - but not very well documented - functions, which we
can use to create custom Redux hooks.

createStoreHook
createDispatchHook
createSelectorHook

If we pass a context into these functions, we can create custom useDispatch and
useSelector functions, that only operate on the ‘local’ / ‘sub’ store, the form store in our
case.

In this post I’m creating a function useFormSelector , but if you’re creating complex
modals you could create useModalDispatch and useModalSelector for example - and
use them if you have a context + store set up as shown above in FormContainer.jsx .

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 15/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

hooks.js

import { useCallback } from "react"


import { createStore } from "redux"
import {
createStoreHook,
createDispatchHook,
createSelectorHook,
useDispatch,
} from "react-redux"

import FormContext from "./context"


import { SOME_GLOBAL_RESET_FORM_ACTION } from "./globalStore"
import { formIdSelector, validationRulesSelector } from "./selectors"
import { validateInput } from "./utils"

/**
* Form reducer constants.
*/
const SET_FORM_ID = "SET_FORM_ID"
const SET_VALUE = "SET_VALUE"
const SET_VALIDATION_RULES = "SET_VALIDATION_RULES"
const RESET_FORM_STATE = "RESET_FORM_STATE"

/**
* Form reducer initial state.
*/
const initialState = {
formId: undefined,
values: {},
validityValues: {},
validationRules: {},
}

/**
* Another standard reducer function.
* This reducer will only handle form actions.
*
* @param {object} state
* @param {object} action
* @returns {object} nextState
*/
const reducer = (state = initialState, { type, payload }) => {
switch (type) {
case SET_FORM_ID:
return { ...state, formId: payload }
https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 16/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

case SET_VALUE:
return {
...state,
values: {
...state.values,
[payload.key]: payload.value,
},
validityValues: {
...state.validityValues,
[payload.key]: payload.isValid,
}
}
case SET_VALIDATION_RULES:
return {
...state,
validationRules: { ...state.validationRules, ...payload },
}
case RESET_FORM_STATE:
return { ...initialState }
default:
return state
}
}

/**
* Redux form store factory function.
*
* Note that 'preloadedState' is not used in this example.
* It's useful when you want to provide a custom initial state
* when writing tests for example.
*
* @param {object} preloadedState Optional, default: undefined
* @returns {object} store
*/
export const createFormStore = (preloadedState) =>
createStore(reducer, preloadedState)

/**
* Rarely used hook for retrieving the form store directly.
* Preferably, use useFormSelector to access store values.
*/
export const useFormStore = createStoreHook(FormContext)

/**
* Form dispatch hook, similar to react-redux's useDispatch hook.
* Actions dispatched using this hook will only affect the specified context.

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 17/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

*/
export const useFormDispatch = createDispatchHook(FormContext)

/**
* Form selector hook, similar to react-redux's useSelector.
* Use this hook to retrieve data from the form store.
*/
export const useFormSelector = createSelectorHook(FormContext)

/**
* Hook for convenient access to the form Redux actions.
*
* @returns {object} formActions
*/
export const useFormActions = () => {
/**
* Use useDispatch and useFormDispatch to be able to
* dispatch actions to both the form store and the global store.
*/
const dispatch = useDispatch()
const formDispatch = useFormDispatch()

/**
* Get (aka select) some values from the form store with 'useFormSelector'.
* It's no problem to use these hooks inside other hooks like this.
*/
const formId = useFormSelector(formIdSelector)
const validationRules = useFormSelector(validationRulesSelector)

/**
* Sets the form id.
*
* @param {string} id
*/
const setFormId = useCallback(
(id) => formDispatch({ type: SET_FORM_ID, payload: id }),
[formDispatch],
)

/**
* Sets a form value and does a validation check.
* We keep track of the value's validity using the 'validityValues' object.
*
* @param {string} key
* @param {string} value
*/

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 18/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

const setValue = useCallback(


(key, value) => {
if (value === undefined) return

const isValid = validateInput(value, validationRules?.[key])

formDispatch({
type: SET_VALUE,
payload: { key, value, isValid },
})
}
[formDispatch, validateInput, validationRules],
)

/**
* Sets the validation rules.
*
* @param {object} validationRules
*/
const setValidationRules = useCallback(
(validationRules) =>
formDispatch({ type: SET_VALIDATION_RULES, payload: validationRules }),
[formDispatch],
)

/**
* Reset the entire form state in the current context.
* And - as an example - also update the global Redux store.
*/
const resetFormValues = useCallback(() => {
formDispatch({ type: RESET_FORM_STATE })
dispatch({ type: SOME_GLOBAL_RESET_FORM_ACTION, payload: formId })
}, [formDispatch, dispatch, formId])

return {
setFormId,
setValue,
setValidationRules,
resetFormValues,
}
}

The icing on the cake 🍰 reselect


(https://github.com/redux/reselect)

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 19/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

Note, these selector functions can be used in both useSelector and useFormSelector !
I would recommend refactoring all existing selectors - even selectors for the ‘global’ store
- to reselect-like selectors.

Organize and create your store value selectors in a central location to be re-used across
multiple components. A great advantage of writing selectors like this with reselect is
that each selector is a ‘pure’ function. This makes sure your data is immutable and state
becomes predictable. It also prevents any potential side effects.

This is a nice example of solving real-world problems using functional programming in


JavaScript (and one of the reasons I like React and Redux in general). We can derive
any new value from existing store values by composing selector functions using
createSelector(...inputSelectors, resultFn) . If the results from the input selectors
don’t change, the selector won’t trigger a re-render in our component.

selectors.js

import { createSelector, createSelectorCreator, defaultMemoize } from "reselect"


import isEqual from "react-fast-compare"

/**
* Creates a custom selector creator function.
* The resulting selector performs a deep equality comparison.
* Uses the 'isEqual' function from 'react-fast-compare'
* Useful for memoization of values of type object or array.
*
* The default 'createSelector' performs strict reference equality comparison (with '
*/
export const createDeepEqualSelector = createSelectorCreator(
defaultMemoize,
isEqual,
)

/**
* Composable memoized Redux selector functions.
* Syntax: createSelector|createDeepEqualSelector(...inputSelectors, resultFn)
* https://github.com/reduxjs/reselect
*
* Each selector must be a 'pure' function.
* A benefit of this is that it makes selectors very reliable and easily testable.
*/
export const formIdSelector createSelector(
https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 20/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog
export const formIdSelector = createSelector(
(state) => state?.formId,
(formId) => formId,
)

export const valuesSelector = createDeepEqualSelector(


(state) => state?.values || {},
(values) => values,
)

export const validationRulesSelector = createDeepEqualSelector(


(state) => state?.validationRules || {},
(validationRules) => validationRules,
)

export const validityValuesSelector = createDeepEqualSelector(


(state) => state?.validityValues || {},
(validityValues) => validityValues,
)

/**
* Before, we would calculate a value like this inside (multiple) components.
* In this new approach we can move the computation to a re-usable selector, like thi
*/
export const requiredFieldsFilledSelector = createSelector(
valuesSelector,
validationRulesSelector,
validityValuesSelector,
(values, validationRules, validityValues) =>
Object.keys(values)
.filter((key) => validationRules?.[key]?.required)
.every((key) => validityValues?.[key] === true),
)

Combining selectors and using props or arguments


https://react-redux.js.org/next/api/hooks#using-memoizing-selectors (https://react-
redux.js.org/next/api/hooks#using-memoizing-selectors)

You can imagine that a form store contains many properties, some of which you want to
combine to create new - more specific - values. Some components don’t need a
subscription to the entire formConfigs object in the following example snippet. It could

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 21/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

be a large object that some components actually do need to subscribe to, but others
might only need a subset of the data from that object.

Generally it’s a good idea to move as much logic to selectors when combining values
from the store. The reducer function preferably shouldn’t do much logic or
computationally expensive operations, as they will fire on every action of that type.
Selectors are a nice way to abstract away store-related logic in a clean and reusable
manner for consumers of store values. You could argue that it’s yet another Redux
boilerplate file + necessary imports, but I think it’s better to write selectors once instead
of re-declaring them in every component that needs that value.

Writing lots of selectors might seem stupid at first, but when selecting a value like
state.cart.cart.consumer.billingAddress.street in a few components, there’s a high
chance to make a small mistake and write
state.cart.consumer.billingAddress.street instead. This is a bug I actually ran into
while refactoring, it had gone unnoticed for almost 6 months.

Defining and having things in a centralised place prevents accidental typo’s and gives
the benefit of being able to make a single change to update all affected components.

selectors.js

// imports, other selectors, etc...

export const formConfigsSelector = createDeepEqualSelector(


(state) => state?.formConfigs || {},
(formConfigs) => formConfigs,
)

export const useShippingAsBillingSelector = createSelector(


(state) => state?.useShippingAsBilling,
(useShippingAsBilling) => useShippingAsBilling,
)

/**
* Example of an polymorphic selector creator.
* (selector which you can pass an argument)
*/
export const createFormConfigSelector = (formId) =>
createDeepEqualSelector(
https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 22/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

formConfigsSelector,
(formConfigs) => formConfigs?.[formId] || {},
)

/**
* Example usage of an polymorphic selector.
*/
export const formConfigShippingSelector = createDeepEqualSelector(
createFormConfigSelector("homeShippingForm"),
(formConfigShipping) => formConfigShipping,
)

/**
* This selector selects a value that has slightly different behavior
* as is often the case in more complex apps.
*/
export const formConfigBillingSelector = createDeepEqualSelector(
createFormConfigSelector("homeBillingForm"),
useShippingAsBillingSelector,
(formConfigBilling, useShippingAsBilling) =>
useShippingAsBilling ? {} : formConfigBilling,
)

export const combinedFormConfigSelector = createDeepEqualSelector(


formConfigShippingSelector,
formConfigBillingSelector,
(formConfigShipping, formConfigBilling) => ({
...formConfigShipping,
...formConfigBilling,
}),
)

/**
* Or:
*/
export const combinedFormConfigSelector = createDeepEqualSelector(
formConfigShippingSelector,
formConfigBillingSelector,
(...args) => Object.assign({}, ...args),
)

/**
* Transforming the selection even further:
*/
export const combinedFormConfigValuesSelector = createDeepEqualSelector(
combinedFormConfigSelector,

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 23/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

Object.values,
)

Optional: Going even further with re-reselect


(https://github.com/toomuchdesign/re-reselect) ♻

Feel free to skip this part, these examples show more advanced usage of selectors. You
probably don’t need this unless you’re dealing with a large, complex and performance
intensive app. We chose not to use re-reselect , so the results described in this article
are based on reselect selectors only.

import { createCachedSelector, LruObjectCache } from "re-reselect"

/**
* This creates a selector that caches based on the 'prefix' value.
* So whenever this selector is called with a previously used prefix it will return t
* previously computed selection, instead of re-evaluating the example 'formatValues'
* The function determining which value the selector should cache on is referred to a
*
* Note that these variable names from the store are totally random and
* have nothing to do with the examples listed above.
*/
const formattedFormValuesSelector = createCachedSelector(
(state) => state.values,
(state, format) => format,
(state, format, prefix) => prefix,
(values, format, prefix) => formatValues(values, format, prefix),
)((state, format, prefix) => prefix) // Cache selectors by prefix

/**
* This is exactly the same.
*/
const formattedFormValuesSelector = createCachedSelector(
(state) => state.values, // a
(state, format) => format, // b
(state, format, prefix) => prefix, // c
(a, b, c) => formatValues(a, b, c), // resultFn
)((state, format, prefix) => prefix) // keySelector

/**
* Or:
*/
const formattedFormValuesSelector = createCachedSelector(
https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 24/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

(state) => state.values,


(state, format) => format,

(state, format, prefix) => prefix,


formatValues, // It's just functional programming!
)((state, format, prefix) => prefix) // Cache selectors by prefix

/**
* Customizing the caching implementation by specifying a custom cache object
* https://github.com/toomuchdesign/re-reselect/tree/master/src/cache#readme
*/
const formattedFormValuesSelector = createCachedSelector(
(state) => state.values,
(state, format) => format,
(state, format, prefix) => prefix,
formatValues,
)({
keySelector: (state, format, prefix) => prefix, // Cache selectors by prefix
cacheObject: new LruObjectCache({ cacheSize: 5 }), // Use the 'LruObjectCache' to c
})

The results

After the rewrite, which took approx. 2 weeks, we saw some nice results.

We saw a decrease in re-renders of about 70-80%! Throughout the typical React


app flow, InputGroup would re-render ~850 times, or more - which we reduced to
~150 times.
The code is cleaner, we fixed a small number of previously unnoticed bugs and
components are small and just do their own job, rendering JSX.
Most data manipulation logic happens very close to the store with ‘pure’ selector
functions, instead of combining and transforming values from React Context in
useEffect hooks.
We moved all (most) hooks to hooks.js files next to the components themselves.
This allows components to simply import their own hooks, or other hooks from a
parent component for example.
And we refactored all (but one) class-based components to functional components.

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 25/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

A good refactor once in a while can avoid a lot of ‘tech-debt’. This is especially true for
projects where developers come and go. Also it revives motivation to work in the
codebase which felt old and messy!

Conclusion

I hope these tricks can benefit you in your projects as much as it did in our case!

If you found this interesting, consider following me on Github


(https://github.com/woudsma). I’m very much open to questions/feedback, don’t hesitate
to send a message or leave a reply. 🙂

A Passionate People (http://passionatepeople.io/) collegue of mine - “The Spider” 🕷 -


made a video on how to tackle the problem of component re-renders without Redux
using the event pattern. Might be worth a watch if you don’t want to / can not use Redux
in your app! Youtube link (https://www.youtube.com/watch?v=xMvJyfNY_Q0).

Links and references


Using custom context hooks (https://react-redux.js.org/next/api/hooks#custom-
context)
Providing custom context (https://react-redux.js.org/using-react-redux/accessing-
store#providing-custom-context) with React Redux
Redux’s reselect (https://github.com/reduxjs/reselect) library (!)
Create cached selectors with re-reselect (https://github.com/toomuchdesign/re-
reselect)

Comments:

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 26/27
2/24/2021 How we reduced our re-renders by 80% with React Redux and custom context - console.blog

0 Comments woudsma 🔒 Disqus' Privacy Policy 


1 Login

 Recommend t Tweet f Share Sort by Best

Start the discussion…

LOG IN WITH
OR SIGN UP WITH DISQUS ?

Name

Be the first to comment.

✉ Subscribe d Add Disqus to your siteAdd DisqusAdd ⚠ Do Not Sell My Data


 (https://github.com/woudsma)

 (mailto:[email protected])

© 2021 Tjerk Woudsma

https://blog.tjerkwoudsma.com/2021/01/29/how-we-reduced-our-re-renders-by-80-with-react-redux-and-custom-context/ 27/27

You might also like