React.js 08: Memoize
Docs
A (Mostly) Complete Guide to React Rendering Behavior
https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/
Problem Statement
What is "Rendering"?
Rendering is the process of React asking your components to describe what they want their section of the UI to look like.
This description is based on the current combination of props and state.
By default, when a component re-renders:
React will recursively re-render all child components inside of it
React recreates all code defined inside a component when it is re-rendered, including functions.
This behavior may create a performance issue when:
rendering takes time, and "wasted renders" can add up - where UI output did not change
a component has en expensive function
a child component renders something expensive
This behavior becomes especially problematic when a component re-render when it actually does NOT have
Solution Statement
We want to
Override that default behavior for any given component.
Skip unnecessary renders for any given component whether triggered by a/ props change or b/ state change
A component re-renders when:
one of its parents gets re-rendered
one of its props gets updated (sent from parent)
one of its states gets updated
In practice, we do NOT want to re-render (skip it) if:
parent re-renders but props are unchanged
a state did not actually change in value — i.e. a recalculation resulting in the same result - we want to cache it somehow
Solution in Code
Memoize:
React.memo()
—> for COMPONENT —> skip re-render if props unchangedReact.useMemo()
—> for VALUE —> cache result of a calculationReact.useCallback()
—> for FUNCTION —> cache a function definition
Should you add memo everywhere?
Yes. Tolerable when used kinda everywhere.
React.memo()
is completely useless if the props passed to your component are always different, such as if you pass an object or a plain function defined during rendering. This is why you will often need useMemo and useCallback together with memo.
useMemo()
Summary
useMemo()
SummaryuseMemo()
hook memoizes VALUES returned by expensive functions
useMemo()
lets you cache the RESULT of a calculation between re-renders.
useMemo()
usage —
useMemo()
usage —const isPrime = useMemo( () => { checkIfPrime(number); },[number]);
React.memo()
usage
React.memo()
usageHigher-order component: component that takes another component as an argument so that it can add functionality to it.
React.memo()
is such "higher-order component".
memo()
lets you skip re-rendering a component when its props are unchanged.
Wrap a component in memo to get a memoized version of that component.
The memoized version will usually not be re-rendered when its parent component is re-rendered as long as its props have not changed.
export const GraphPoint = () => { const [on] = useState(Math.random() > 0.8);
return <div className={
e3-graph-point ${on ? 'e3-graph-point-on' : ''}
} />; };
Becomes
export const GraphPoint = React.memo(() => { const [on] = useState(Math.random() > 0.8);
return <div className={
e3-graph-point ${on ? 'e3-graph-point-on' : ''}
} />; });
React.useCallback()
usage
React.useCallback()
usageuseCallback()
allows us to memoize a FUNCTION, preventing React from recreating that function when the component re-renders.
This hook is particularly important when passing functions as props to components memoized with React.memo()
. Since React.memo()
compares props between renders, it checks if each prop is the same before and after each render.
The problem is that in JavaScript, two identical functions will not equal each other since they are stored in the language as two separate references.
useCallback()
complementsReact.memo()
— never use it woReact.memo()
being used elsewhere
Original Code
const CounterContainer = () => { const [count, setCount] = useState(0); function increment () { setCount((count) => count + 1); } return }
Becomes
const CounterContainer = () => { const [count, setCount] = useState(0); const increment = useCallback(() => setCount((count) => count + 1), []); return }
Code Splitting: Docs
https://javascript.info/modules-dynamic-imports
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Async_JS/Introducing
https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Async_JS/Promises
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
https://react.dev/reference/react/lazy
Code Splitting: Problem Statement
Sometime some code is expensive to load. We would like to delay its loading to only when needed.
We need: a technique to only send the necessary parts of our React code when the user needs them.
FYI: bundle.js
contains the packaged javascript
Warning: right-click "empty cache & reload" when refreshing page to measure file size of bundle.js
Using await import()
await import()
The import()
syntax is based on JavaScript Promises, so we use await
to wait for the Promise to resolve and mark the function as async
. Also, we have to add .default
to the imported code after importing it
Regular import
import moment from 'moment'; function onClick() { setDate(moment(new Date()).format('MM/D/YYYY')) }
Becomes
async function onClick() { const moment = await import('moment'); // note the .default setDate(moment.default(new Date()).format('MM/D/YYYY')) }
Regular import
import getIconOptions from './getIconOptions'; const load = () => { const userIconOptions = getIconOptions(); setIconOptions(userIconOptions); };
Becomes
const load = async () => { const getIconOptions = await import('./getIconOptions'); const userIconOptions = getIconOptions.default(); setIconOptions(userIconOptions); }
Promises: Problem Statement
When doing JS asynchronous programming with callbacks, you often have to call callbacks inside callbacks (nested callbacks).
We get a deeply nested doOperation() function, which is much harder to read and debug.
It can also get very hard to handle errors at specific levels, vs error handling only once at the top level.
This is sometimes called "callback hell" or the "pyramid of doom" (coz the indentation looks like a pyramid on its side).
We need something cleaner.
Promises: Clean Syntax For Async Programming
Promises are the foundation of asynchronous programming in modern JavaScript.
A promise is an object returned by an asynchronous function, which represents the current state of the operation.
At the time the promise is returned to the caller, the operation often is not finished, but the promise object provides methods to handle the eventual success or failure of the operation.
Promise: using .then()
and .catch()
.then()
and .catch()
Not so easy to read at first but gets easier as you use promises.
Remember the similarity to event handlers.
Except we can call the second then() on that return value. This is called promise chaining.
const fetchPromise = fetch( "bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => { if (!response.ok) { throw new Error(
HTTP error: ${response.status}
); } return response.json(); }) .then((data) => { console.log(data[0].name); }) .catch((error) => { console.error(Could not get products: ${error}
); });
Promise: using async, await and catch
The async
keyword gives you a simpler way to work with asynchronous promise-based code.
Adding async
at the start of a function makes it an async function
:
async function fetchProducts() { try { const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(
HTTP error: ${response.status}
); } const data = await response.json(); console.log(data[0].name); } catch (error) { console.error(Could not get products: ${error}
); } }
Using React.lazy() with
React.lazy()
lets you defer loading component’s code until it is rendered for the first time.
Regular import
import DrivingDirectionsMap from './DrivingDirectionsMap';
Becomes
const DrivingDirectionsMap = React.lazy(() => import('./DrivingDirectionsMap'));
React.lazy()
must be put after all the imports
React.lazy()
must be put after all the importsRegular component
Becomes
import Loader from './Loader'; <Suspense fallback={}>
Last updated