JavaScript and React course Β· Module 5: useEffect and Custom Hooks

Memoization - React.memo, useMemo, and useCallback

6 min read
In this lesson8

In outer space, resources are limited and must be used wisely. Similarly in the React world, application performance is of key importance. Memoization is an optimization technique that allows "remembering" results of expensive operations and reusing them when input data hasn't changed - similar to saving and reusing trajectory calculations instead of performing them from scratch.

What is memoization in React?

Memoization in React is a performance optimization mechanism that allows you to:

  • Avoid unnecessary component re-renders
  • Prevent re-execution of expensive calculations
  • Stabilize references to functions and objects

React.memo - component memoization

React.memo is a higher-order component (HOC) that wraps a component and prevents it from re-rendering if its props haven't changed. zmianie.

Basic usage of React.memo

1import React from 'react';
2
3// Component without memoization
4function PlanetInfo({ name, diameter, distance }) {
5  console.log(`Rendering planet info: ${name}`);
6  return (
7    <div className="planet-info">
8      <h2>{name}</h2>
9      <p>Diameter: {diameter} km</p>
10      <p>Distance from Sun: {distance} million km</p>
11    </div>
12  );
13}
14
15// Same component with memoization
16const MemoizedPlanetInfo = React.memo(PlanetInfo);
17
18// Parent component
19function SolarSystem() {
20  const [count, setCount] = useState(0);
21
22  return (
23    <div>
24      <button onClick={() => setCount(count + 1)}>
25        Counter: {count}
26      </button>
27
28      {/* This component will only re-render when its props change */}
29      <MemoizedPlanetInfo
30        name="Mars"
31        diameter={6779}
32        distance={228}
33      />
34    </div>
35  );
36}

Notice that in the above example, even though the parent component SolarSystem renders again on every button click, the component MemoizedPlanetInfo will not re-render because its props remain unchanged.

Custom props comparison

By default React.memo compares props using shallow comparison. However, you can provide your own comparison function:

1const MemoizedPlanetInfo = React.memo(
2  PlanetInfo,
3  (prevProps, nextProps) => {
4    // Return true if you want to prevent re-rendering
5    // Return false if you want to re-render the component
6    return prevProps.name === nextProps.name &&
7           prevProps.diameter === nextProps.diameter;
8    // Note that we ignore changes to distance - the component won't re-render
9    // when only this property changes
10  }
11);

useMemo - value memoization

Hook useMemo allows remembering the result of an expensive operation and reusing it as long as dependencies haven't changed.

When to use useMemo?

  1. For expensive calculations
  2. When creating reference objects as props for child components
  3. When creating large data structures
1import React, { useState, useMemo } from 'react';
2
3function AsteroidCalculator({ asteroids }) {
4  const [filter, setFilter] = useState('all');
5
6  // Expensive calculation - memoized for better performance
7  const dangerousAsteroids = useMemo(() => {
8    console.log('Calculating dangerous asteroids...');
9    return asteroids.filter(asteroid => {
10      // Simulation of expensive calculations
11      let isDangerous = false;
12      for (let i = 0; i < 10000; i++) {
13        if (asteroid.velocity > 20 && asteroid.size > 1000 && asteroid.distance < 50000000) {
14          isDangerous = true;
15        }
16      }
17      return isDangerous;
18    });
19  }, [asteroids]); // Calculation will be re-executed only when the asteroids array changes
20
21  // This calculation is also memoized but depends on the filter
22  const displayedAsteroids = useMemo(() => {
23    console.log('Filtering asteroids...');
24    if (filter === 'all') return asteroids;
25    if (filter === 'dangerous') return dangerousAsteroids;
26    return [];
27  }, [filter, asteroids, dangerousAsteroids]);
28
29  return (
30    <div>
31      <div>
32        <button onClick={() => setFilter('all')}>All</button>
33        <button onClick={() => setFilter('dangerous')}>Nobezpieczne</button>
34      </div>
35      <h2>Asteroids ({displayedAsteroids.length}):</h2>
36      <ul>
37        {displayedAsteroids.map(asteroid => (
38          <li key={asteroid.id}>{asteroid.name}</li>
39        ))}
40      </ul>
41    </div>
42  );
43}

useCallback - function memoization

Hook useCallback is similar to useMemo, but instead of remembering the function's result, it remembers the function itself. It's particularly useful when passing functions as props to memoized child components.

1import React, { useState, useCallback } from 'react';
2
3function MissionControl() {
4  const [missionStatus, setMissionStatus] = useState('preparing');
5  const [fuelLevel, setFuelLevel] = useState(100);
6
7  // Z useCallback, function is recreated only when fuelLevel changes
8  const startMission = useCallback(() => {
9    if (fuelLevel > 20) {
10      setMissionStatus('in-progress');
11      console.log(`Mission started with fuel level: ${fuelLevel}%`);
12    } else {
13      console.log('Not enough fuel to start mission');
14    }
15  }, [fuelLevel]); // Dependency: function recreated when fuelLevel changes
16
17  const abortMission = useCallback(() => {
18    setMissionStatus('aborted');
19    console.log('Mission aborted!');
20  }, []); // Empty dependency array - function created only once
21
22  return (
23    <div>
24      <h2>Mission status: {missionStatus}</h2>
25      <p>Fuel level: {fuelLevel}%</p>
26
27      <button onClick={() => setFuelLevel(prev => Math.max(0, prev - 10))}>
28        Use fuel
29      </button>
30
31      {/* Passing memoized functions as props */}
32      <MemoizedControlPanel
33        onStart={startMission}
34        onAbort={abortMission}
35        fuelLevel={fuelLevel}
36      />
37    </div>
38  );
39}
40
41// Memoized child component that receives functions as props
42const ControlPanel = React.memo(({ onStart, onAbort, fuelLevel }) => {
43  console.log('Rendering control panel');
44
45  return (
46    <div className="control-panel">
47      <h3>Control panel</h3>
48      <button onClick={onStart} disabled={fuelLevel <= 20}>
49        Start
50      </button>
51      <button onClick={onAbort}>
52        Abort mission
53      </button>
54    </div>
55  );
56});
57
58const MemoizedControlPanel = React.memo(ControlPanel);

Comparing memoization techniques and when to use them

TechniqueUse caseWhen to use
React.memoComponentWhen a component frequently receives the same props
useMemoValueFor expensive calculations and complex data structures
useCallbackFunctionWhen passing functions as props to memoized components

Common mistakes and pitfalls

1. Over-memoization

1// Not optimal - too simple a value to memoize
2const count = useMemo(() => 1 + 2, []);
3
4// Not optimal - simple function that doesn't need memoization
5const handleClick = useCallback(() => {
6  console.log('Clicked!');
7}, []);

2. Forgetting dependencies

1// Not safe - missing dependency 'value'
2const processedData = useMemo(() => {
3  return heavyProcessing(value);
4}, []); // Should be [value]

3. Too many dependencies

1// May reduce memoization effectiveness
2const result = useMemo(() => {
3  return computeResult(a, b);
4}, [a, b, c, d, e]); // Too many dependencies - memoization may be ineffective

Use memoization wisely

  1. Don't optimize prematurely - use profilers first to identify actual performance issues
  2. Apply memoization selectively - only where it's actually needed
  3. Remember the cost - memoization also has its cost, which may outweigh the gains for simpler operations

Summary

Memoization in React is a powerful optimization technique that - like conserving resources on a spaceship - helps efficiently manage application performance. Use it wisely:

  • Use React.memo for components that frequently re-render with the same props
  • Use useMemo for expensive calculations and creating complex data structures
  • Use useCallback to stabilize function references passed down the component tree

However, remember that like any optimization technique, memoization has its cost and should be applied judiciously, where it actually brings performance benefits.

Code for this lesson: App.jsx
1import React, { useState, useMemo, useCallback, memo, useRef } from 'react';
2
3// React.memo - component memoization
4const SpaceShip = memo(function SpaceShip({ name, speed, onSelect }) {
5  console.log(`Rendering SpaceShip: ${name}`);
6
7  return (
8    <div className="spaceship" onClick={() => onSelect(name)}>
9      <h3>{name}</h3>
10      <p>Speed: {speed} km/s</p>
11    </div>
12  );
13});
14
15// useMemo - value memoization
16function ExpensiveCalculation({ numbers }) {
17  const [filter, setFilter] = useState('');
18
19  // useMemo prevents recalculation on every render
20  const stats = useMemo(() => {
21    console.log('Calculating statistics...');
22
23    // Simulating an expensive operation
24    const sum = numbers.reduce((a, b) => a + b, 0);
25    const avg = sum / numbers.length;
26    const max = Math.max(...numbers);
27    const min = Math.min(...numbers);
28
29    return { sum, avg, max, min };
30  }, [numbers]); // Only when numbers changes
31
32  const filteredNumbers = useMemo(() => {
33    if (!filter) return numbers;
34    const threshold = parseInt(filter) || 0;
35    return numbers.filter(n => n >= threshold);
36  }, [numbers, filter]);
37
38  return (
39    <div className="demo-card">
40      <h2>useMemo - Statistics</h2>
41      <div className="stats">
42        <div className="stat">Sum: {stats.sum}</div>
43        <div className="stat">Average: {stats.avg.toFixed(2)}</div>
44        <div className="stat">Min: {stats.min}</div>
45        <div className="stat">Max: {stats.max}</div>
46      </div>
47      <input
48        type="number"
49        value={filter}
50        onChange={(e) => setFilter(e.target.value)}
51        placeholder="Filter >= value"
52      />
53      <div className="filtered">
54        Filtered: [{filteredNumbers.join(', ')}]
55      </div>
56    </div>
57  );
58}
59
60// useCallback - function memoization
61function SpaceFleet() {
62  const [ships] = useState([
63    { name: 'Apollo', speed: 11 },
64    { name: 'Voyager', speed: 17 },
65    { name: 'Enterprise', speed: 299792 },
66  ]);
67  const [selected, setSelected] = useState(null);
68  const [count, setCount] = useState(0);
69
70  // Without useCallback - a new function on every render
71  // const handleSelect = (name) => setSelected(name);
72
73  // With useCallback - the same function reference
74  const handleSelect = useCallback((name) => {
75    setSelected(name);
76  }, []);
77
78  return (
79    <div className="demo-card">
80      <h2>useCallback - Fleet</h2>
81      <p className="hint">Open the console - see which ships re-render</p>
82
83      <div className="fleet">
84        {ships.map(ship => (
85          <SpaceShip
86            key={ship.name}
87            name={ship.name}
88            speed={ship.speed}
89            onSelect={handleSelect}
90          />
91        ))}
92      </div>
93
94      {selected && <p className="selected">Selected: {selected}</p>}
95
96      <div className="counter-section">
97        <button onClick={() => setCount(c => c + 1)}>
98          Counter: {count} (causes re-render)
99        </button>
100      </div>
101    </div>
102  );
103}
104
105// Comparison with and without memoization
106function MemoizationComparison() {
107  const [value, setValue] = useState(0);
108  const calls = useRef({ with: 0, without: 0 });
109
110  // Function WITHOUT memoization
111  const expensiveWithout = () => {
112    calls.current.without += 1;
113    let result = 0;
114    for (let i = 0; i < 1000; i++) {
115      result += Math.sqrt(i);
116    }
117    return result;
118  };
119
120  // Function WITH memoization
121  const expensiveWith = useMemo(() => {
122    calls.current.with += 1;
123    let result = 0;
124    for (let i = 0; i < 1000; i++) {
125      result += Math.sqrt(i);
126    }
127    return result;
128  }, []); // Empty array = calculate once
129
130  const resultWithout = expensiveWithout();
131
132  return (
133    <div className="demo-card">
134      <h2>Comparison</h2>
135      <div className="comparison">
136        <div className="box without">
137          <h3>Without useMemo</h3>
138          <p>Calls: {calls.current.without}</p>
139          <p className="result">{resultWithout.toFixed(2)}</p>
140        </div>
141        <div className="box with">
142          <h3>With useMemo</h3>
143          <p>Calls: {calls.current.with}</p>
144          <p className="result">{expensiveWith.toFixed(2)}</p>
145        </div>
146      </div>
147      <button onClick={() => setValue(v => v + 1)}>
148        Force re-render ({value})
149      </button>
150    </div>
151  );
152}
153
154function App() {
155  const [numbers] = useState([23, 45, 12, 67, 89, 34, 56, 78, 90, 11]);
156
157  return (
158    <div className="app">
159      <h1>Memoization in React</h1>
160      <p className="subtitle">React.memo, useMemo, useCallback</p>
161      <div className="demos">
162        <ExpensiveCalculation numbers={numbers} />
163        <SpaceFleet />
164        <MemoizationComparison />
165      </div>
166    </div>
167  );
168}
169
170export default App;

Spotted a mistake in this lesson?

Hands-on tasks in the game

  • Horizontal ordering

    Arrange the correct useMemo syntax for memoizing computations:

Useful articles