Optimize React Components Using Memoization

Tanveer Karim
Mar 4, 2020

Memoization is a technique that stores the results of a function based on the provided input. If the function is called multiple times with the same inputs, the stored results are returned each time. Memoization can be a great way to improve the performance of a React application. In fact, there is a built-in hooks, useMemo, that you can use to memoize your components.

Link

Why memoize?

In the example below, the React component performs a hypothetical expensive calculation inside the expensiveOperation function. It's a simple addition, but for demo purposes let's pretend it's a complex equation. It's important to remember that the value returned by this function never changes, because the arguments passed to it are always the same.

Inside our component is a button which updates the callCount state when clicked. Each time you click the button, the component will re-render and expensiveOperation will be called.

// Hypothetical expensive operation
const expensiveOperation = (oa, ob) => {
  alert(`expensiveOperation(${oa}, ${ob})`);
  return oa + ob;
};

export const ExpensiveComponent = () => {
  // Component will re-render when callCount is updated
  const [callCount, setCallCount] = useState(0);
  const a = 100;
  const b = 250;
  // This returned value never changes when component is re-rendered
  const expensiveValue = expensiveOperation(a, b);
  return (
    <div>
      <button onClick={(e) => setCallCount(callCount + 1)}>Count</button>
      <p>Count: {callCount}</p>
      <p>Memoized Value: {expensiveValue}</p>
    </div>
  );
};

To test this yourself, try the live demo below. I've added an alert dialog inside the expensiveOperation function. Whenever you click the Count button, you will see an alert dialog, and the count value will increase by one.

Now if expensiveOperation was actually performing a long and complicated task, we would have a very poorly optimized component on our hand. But if we used memoization, we only need to call the function once to perform our calculation and cache the result. And since the arguments do not change on every subsequent re-render, we only return the cached value.

Link

Memoization via useMemo Hook

In the next example, we have the same component, but this time we are wrapping the expensiveOperation function with the useMemo hook.

const expensiveOperation = (oa, ob) => {
  alert(`expensiveOperation(${oa}, ${ob})`);
  return oa + ob;
};

const ExpensiveComponent = () => {
  const [callCount, setCallCount] = useState(0);
  const a = 100;
  const b = 250;
  const memoizedValue = useMemo(
    () => expensiveOperation(a, b), 
    [a, b]
  );
  return (
    <div>
      <button onClick={(e) => setCallCount(callCount + 1)}>Count</button>
      <p>Count: {callCount}</p>
      <p>Memoized Value: {memoizedValue}</p>
    </div>
  );
};

The useMemo hook will call the expensiveOperation function and cache its return value based on the hook dependencies. If the hook dependencies remain the same, it will return the cached value. Try out the demo below.

The alert dialog will appear once, because each subsequent re-render now returns the stored value instead of calling expensiveOperation.

Link

Using useMemo

The useMemo hook takes two arguments: a create function and an array of dependencies. The create function should return the value you want to cache, and the array of dependencies should contain the arguments passed to the expensive function. In the example below, the create function is returning the result of the expensive function.

// Our function that performs some long and difficult calculation
const expensiveFunction = (propA, propB, propC) => {...}

// Function arguments
const argA = 1;
const argB = 2;
const argC = 3;

// First argument is the "create" function
// Second argument is the array of dependencies
const memoValue = useMemo(
  // "Create" function that returns
  // the result of our expensive function.
  () => expensiveFunction(argA, argB, argC),
  // Hook dependencies - an array of values. 
  // If these change, then expensiveFunction will be re-calculated
  [argA, argB, argC]
);

During a component re-render, if the dependency remains the same, useMemo will always return the cached value. However, if one or more of the dependencies changes, then the expensive function will be called and value will be recalculated. If no array of dependencies are provided, then the expensive function will be called on every component re-render.

The useMemo hook is a great way to optimize computationally expensive functions. But there is also another use case - reference equality.

Link

Reference Equality

Disclaimer before we continue:

The explanation below simplifies a lot of what Javascript actually does, but it will give you a general idea of how primitives and objects are compared. If you want to learn more, MDN has a thorough explanation of equality checks in JavaScript.
...

Javascript objects (including arrays and functions) are assigned by reference. This means that a variable stores the reference to the object's location in memory, not the actual object. Hence, reference equality is checking if two object references are the same. In the example below, two variables are initialized with identical objects. Even though the objects are identical, they will be stored separately in memory, and a different reference will be assigned to variable objA and objB. A reference equality check on objA and objB will always return false, because you are comparing the object references, not their values.

// Both objects have identical "values"
let objA = {one: 1};
let objB = {one: 1};

console.log(objA === objB); // false

In contrast, primitive values, such as numbers or strings, are assigned by value. So comparing two primitive values will compare their actual values.

// Both primitives identical values
let a = 150;
let b = 150;
a === b; // true

When you pass the object to a function or assign it to a new variable, you are only passing the reference to the new variable or function parameters, not the actual value. The reference will be copied to the new variable or function parameter.

let objA = {one: 1};
// Reference of objA will be copied to copyA
let copyA = objA;

console.log(copyA === objA); //true

// Reference of objA will be copied to objB
// in the function parameter
checkReference(objA);
function checkReference(objB) {
  let objC = {one: 1};
  console.log(objA === objB); // true
  console.log(objB === objC); // false
}

Let's look at another example. Inside myExampleFunc we are creating an object and assigning a reference to myOjbProp. Every time we call this function, the object is recreated, and a new reference is assigned to myOjbProp. When we return the value of myOjbProp, we are returning the reference to the object. Thus every new function call returns a new reference. This is why the reference equality of objA and objB will always be false.

const myExampleFunc = () => {
  let myObjProp = {a: 1};
  return myObjProp;
}
let objA = myExampleFuncObj();
let objB = myExampleFuncObj();

objA === objB; // false

Now, instead of returning the object, what if we passed it to another function? Every time the parent function is called, we would pass a new reference to the child function.

const myNestedFunc = (paramA) => {}

const myExampleFunc = () => {
  let myObjProp = {a: 1};
  // A new reference will be passed
  // to the nested function everytime
  // myExampleFunc is called
  myNestedFunc(myOjbProp);
}

myExampleFuncObj();
myExampleFuncObj();

Let's take a look at how this relates to a React component.

In the example below, we have a React functional component, with a nested child component. The ParentComponent has a variable propA which stores a reference to an object. This reference is passed to the ChildComponent as a prop. Every time the parent component is re-rendered, the object is recreated, and a new reference is passed down to the child component.

const ChildComponent = ({ childPropA }) => {
  ...
};


const ParentComponent = () => {
  const propA = {showExcitement: true};
  
  return <ChildComponent childPropA={propA} />
}

Expanding on our example, we now have ChildComponent utilizing the useEffect hook. This hook has a "create" function that updates the useEffectCount state. It is also using the childPropA as a hook dependency. Based on the rules of the hook, the create function will be called the first time the ChildComponent is rendered, and every time the value of childPropA has changed.

For every re-render, the useEffect hook will check the reference equality of childPropA, comparing the previous render state to the current render state. If the references of childPropA are not equal between each re-render, the useEffect hook will call the "create" function, causing us to update the state of ChildComponent.

The "create" function is the first argument you pass to a hook, e.g: useEffect(createFunction, dependencies).
const ChildComponent = ({ childPropA }) => {
  const [useEffectCount, setUseEffectCount] = useState(0);
  useEffect(() => {
    setUseEffectCount((count) => count + 1);
    // Hook dependencies
  }, [childPropA]);
  return <div>useEffectCount: {useEffectCount}</div>;
};

const ParentComponent = () => {
  const isExcited = true;
  const propA = { showExcitement: isExcited };

  const [demoState, setDemoState] = useState(0);
  return (
    <>
      <ChildComponent childPropA={propA} />
      <button onClick={() => setDemoState((v) => v + 1)}>
        Re-render ParentComponent (demoStateCount: {demoState}x)
      </button>
    </>
  );
};

For demo purposes, I've added a button that updates the state of the ParentComponent, causing it to re-render. Try it out below - every time you click the button, the useEffectCount and the demoStateCount will increment.

So what's the problem here? When ParentComponent re-renders, the reference passed to the ChildComponent always changes. This unintentionally causes a state change in the child component. What if the useEffect hook was doing an expensive operation like a HTTP request instead of a state change? We would be unintentionally performing this expensive operation, even though we did not need to.

Link

Reference equality and memoization

This is where memoization comes into play. We can use the useMemo hook to preserve our object reference during each re-render, ensuring we are always passing the same reference to the child component. When the useEffect hook checks the reference equality of childPropA, we can be sure it will not cause an unintended re-render.

To memoize our object reference, we simply return the object using the "create" function in our useMemo hook.

const isExcited = true;
const propA = useMemo(
  () => {
  showExcitement: isExcited;
  },
  [isExcited]
);
const ChildComponent = ({ childPropA }) => {
  const [useEffectCount, setUseEffectCount] = useState(0);
  useEffect(() => {
    setUseEffectCount((count) => count + 1);
    // Hook dependencies
  }, [childPropA]);
  return <div>useEffectCount: {useEffectCount}</div>;
};

const ParentComponent = () => {
  const isExcited = true;
  const propA = useMemo(() => {
    showExcitement: isExcited;
  }, [isExcited]);

  const [demoState, setDemoState] = useState(1);
  return (
    <>
      <ChildComponent childPropA={propA} />
      <button onClick={() => setDemoState((v) => v + 1)}>
        Re-render ParentComponent (demoStateCount: {demoState}x)
      </button>
    </>
  );
};

Try out the demo below. With this modification, the useEffectCount will only increment once.

Using useMemo to preserve reference equality is a simple yet powerful way to optimize your Reach application.

Link

What not to do

One thing you may be tempted to do, but should avoid, is omitting dependencies that are being used inside the hook. The React documentation quotes:

The array of dependencies is not passed as arguments to the function. Conceptually, though, that’s what they represent: every value referenced inside the function should also appear in the dependencies array.

In fact, there is the exhaustive-deps check that comes with eslint-plugin-react-hooks, which verifies if dependencies are being used correctly. It is a good idea to use this rule in your project if you have not done so already. Dan Abramov has also warned against disabling this rule.

Another thing to consider - don't use anything that may have side effects (like an AJAX call) inside the memoization hook. Those should be used inside the useEffecthook.

Finally, consider the cost of memoization before you decide to use it. Are you declaring a lot of variables or performing tasks with long execution times? If not, then memoization may not be worth it. The extra overhead for using the hook may end up costing more resources than calling a function directly.