As web applications become more and more complex, they often become increasingly bloated, making the app less responsive. As developers, we must start to think about how to optimize our app so that they provide better user experience.
In this lesson, we are going to cover some of the tools React has provided us to help optimize our apps.
Memorizing the result of a function
Sometimes you may have functions performing "expensive" tasks in your program, operations that consume significant time or system resources. In this case, you could let React memorize the result of that function with the useMemo()
hook:
1import { useState, useMemo } from "react";
2
3function SquareCalculator() {
4 const [number, setNumber] = useState(0);
5 const [counter, setCounter] = useState(0);
6
7 const squaredNum = useMemo(
8 function square() {
9 console.log("Calculating square...");
10 return number * number;
11 },
12 [number]
13 );
14
15 return (
16 <>
17 <div>
18 <input
19 type="number"
20 value={number}
21 onChange={(e) => setNumber(Number(e.target.value))}
22 />
23 <p>
24 Square of {number}: {squaredNum}
25 </p>
26 </div>
27
28 <div>
29 <p>Counter: {counter}</p>
30 <button onClick={() => setCounter(counter + 1)}>
31 Increment Counter
32 </button>
33 </div>
34 </>
35 );
36}
37
38export default SquareCalculator;
In this example, pay attention to line 7 to 13. The function square()
will calculate and return the square of number
. The returned value will be saved to the variable squaredNum
. As lone as number
does not change, the function square()
will not be executed. The saved value will be preserved between renders.
To verify this, open the browser console and type inside the input box. This will update number
, and causing square()
to recalculate. You should see Calculating square...
being logged to the console.
However, when you click on the Increment Counter button, this will cause the component to be rendered, but notice that the function square()
is not executed, and the saved value, squaredNum
, is not lost either.
Memorizing a component
Besides memorizing the result of a function, it is also possible for React to memorize an entire component. memo()
allows a component to skip re-rendering, as long as its props do not change. For example:
1import { memo, useState } from "react";
2
3function App() {
4 const [fruit, setFruit] = useState("");
5 const [price, setPrice] = useState("");
6
7 return (
8 <>
9 <input
10 type="text"
11 name="fruit"
12 id="fruit"
13 value={fruit}
14 onChange={(e) => setFruit(e.target.value)}
15 placeholder="Fruit"
16 />
17 <input
18 type="text"
19 name="price"
20 id="price"
21 value={price}
22 onChange={(e) => setPrice(e.target.value)}
23 placeholder="Price"
24 />
25 <Fruit name={fruit} />
26 </>
27 );
28}
29
30const Fruit = memo(({ name }) => {
31 console.log("Component re-rendered.");
32 return <p>Fruit: {name}</p>;
33});
34
35export default App;
Take a closer look at Fruit
. Instead of defining the Fruit
component as a regular function, it is wrapped inside memo()
. The component accepts name
as its prop.
Type inside the first input box, which changes the value of name
. Notice that Fruit
will be re-rendered every time. You should see Component re-rendered.
being printed to the console.
But when you type inside the second input field, which updates price
. This will also cause App
to be re-rendered, but not Fruit
. Nothing should be printed to the console in this case.
Memorizing a function
By default, when a component is re-rendered, functions defined inside that component will be recreated. Using the useCallback()
hook, React allows you to memorize a function between renders.
This may sound like a strange things to do, as declaring a function doesn't really consume too much resources. So what exactly is the purpose of useCallback()
?
useCallback()
is often used in combination with memo()
to prevent unnecessary re-renders, when functions are passed to components as props. For example:
1import { useState, useCallback, memo } from "react";
2
3function App() {
4 const [count, setCount] = useState(0);
5 const [name, setName] = useState("");
6
7 const incrementCount = useCallback(() => {
8 setCount((prevCount) => prevCount + 1);
9 }, []);
10
11 console.log("App rendered");
12
13 return (
14 <>
15 <h1>Counter App</h1>
16 <input
17 value={name}
18 onChange={(e) => {
19 setName(e.target.value);
20 }}
21 />
22 <CounterDisplay count={count} />
23 <CounterButton incrementCount={incrementCount} />
24 </>
25 );
26}
27
28const CounterDisplay = memo(({ count }) => {
29 console.log("CounterDisplay rendered");
30 return <p>Current Count: {count}</p>;
31});
32
33const CounterButton = memo(({ incrementCount }) => {
34 console.log("CounterButton rendered");
35 return <button onClick={incrementCount}>Increment Count</button>;
36});
37
38export default App;
In this case, App
has two child components, CounterDisplay
and CounterButton
, each wrapped inside memo()
. As long as their respective props do not change, these components should not be re-rendered when App
is re-rendered.
However, there is a problem with CounterButton
. It accepts a function as its prop. By default, when App
is re-rendered, the function will be recreated. React will treat them as two different values, causing CounterButton
to re-render every time, making memo()
essentially pointless.
To solve this problem, you can wrap that function inside useCallback()
. React will memorize this function across renders, and it will no longer be recreated unless the specified dependency is changed.
For the above demo, when App
is first rendered, you should see all three messages being printed to the console.
When you type in the input field, App
will be rendered, but CounterDisplay
and CounterButton
will be skipped.
When you click the Increment Count button, count
will be updated, causing CounterDisplay
to be re-rendered, but CounterButton
will still be skipped.
Lazy loading and suspense
Lazy loading is a common technique when optimizing web applications. React allows you to lazily load a component and display a temporary fallback while it is waiting.
1import { Suspense, lazy } from "react";
2
3const LazyComponent = lazy(() => import("./components/LazyComponent"));
4
5function App() {
6 return (
7 <div>
8 <h1>React Lazy Loading and Suspense</h1>
9 <Suspense fallback={<div>Loading...</div>}>
10 <LazyComponent />
11 </Suspense>
12 </div>
13 );
14}
15
16export default App;
components/LazyComponent.jsx
Instead of importing the LazyComponent directly, you need to use lazy()
, which accepts a load function like this:
1const LazyComponent = lazy(() => import("./components/LazyComponent"));
This lazily imported component can be displayed using the same JSX syntax we are all familiar with:
1<LazyComponent />
To display a fallback while waiting for LazyComponent
to load, you need to use <Suspense>
. This built-in requires a prop fallback
, which accepts a piece of JSX that will be displayed while the component is being loaded.
1<Suspense fallback={<div>Loading...</div>}>
2 <LazyComponent />
3</Suspense>
However, one thing to note is that <Suspense>
is a relatively new addition to React, and currently, it does not detect data fetching inside useEffect()
or event handlers. Right now, <Suspense>
is only useful when you use a suspense-enabled, React-based, fullstack framework such Next.js, which we are going to discuss in future lessons.