paint-brush
How to Use the Memo Feature in React Applications: Accuracy With React Memorizationby@antonkalik
133 reads

How to Use the Memo Feature in React Applications: Accuracy With React Memorization

by Anton KalikMay 11th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

This article provides a detailed explanation of how to use the memo feature in React applications. It also includes an exploration of how it works behind the scenes. For more information, visit the React documentation site or read the code in the README.com version of the article that includes this article.
featured image - How to Use the Memo Feature in React Applications: Accuracy With React Memorization
Anton Kalik HackerNoon profile picture

This article provides a detailed explanation of how to use the memo feature in React applications, including an exploration of how it works behind the scenes.

The idea of returning data that was already stored is not new, but understanding how it works under the hood is very important for all developers.


The memo is memory, and that development technic helps to optimize the calculation process for expensive functions. The usage of memorization is a very important practice and is laid as crucial knowledge for development in general. React has good instruments for memorization.


But how does it work, why is it important, and how do you use it? Let’s find it out in this article in detail with practical examples.


What Is Memorization?

Memorization is the function that just has a store. Closure helps store data and take it from there by key to get the result. Very simple idea: key and value.


Memorization Schema



Let’s say that we have a function that has two number arguments, and the result is an addition of both of those arguments. What do we do? We’ll pass this as an argument this function to the memo function.


With the below function, we’ll create a memory property as an empty object and then returned the new function which is going to expect arguments. We’ll use those arguments as a key for storing and returning our results from the memorized function.


function memorize(fn) {
  let memory = {};
  return function (...args) {
    let key = JSON.stringify(args) || 'no_args';
    if (memory[key]) {
      return memory[key];
    }
    let result = fn.apply(this, args);
    memory[key] = result;
    return result;
  };
}


You can see that after other calls of the memorized function, there is a condition where we check the existing results based on keys from stringified arguments. If the result already exists, we just return the result itself.

How to Use It

First of all, let’s put some console.log in our function to understand better the state changes.

// ...
let result = fn.apply(this, args);
console.log('result', result);
memory[key] = result;
// ...


Now, let’s create a function for memo:

function addition(a, b) {
  return a + b;
}


Time to memorize it and reuse the function multiple times, and check how many times the memorized function was called actually.

const memorizedAddition = memorize(addition);

console.log(memorizedAddition(1, 2));
console.log(memorizedAddition(2, 2));
console.log(memorizedAddition(1, 2));
console.log(memorizedAddition(1, 2));
console.log(memorizedAddition(3, 3));


We created the memorized function and called console.log five times but have passed the same arguments in three invocations.

result 3
result 4
result 6


The memorized function will be called only three times because there is no need to call the function again if the result is already known — in which case, the result will be returned from memory.

Applying to a React Component

What happens if we apply this to react component?


Let’s create a very simple component in a React application using yarn create vite without React.StrictMode.


Strict mode is not our target. It only makes additional rendering for highlighting potential problems from inherited APIs — for instance, some imported components. We’ll not use any libraries. Instead, we’ll focus on the render process.


let key = JSON.stringify(args) || "no_args";
console.log("key", key);


Let’s put another console.log to our memorize function, and run the app.


const Block = ({ count }) => {
  return (
    <div>
      <h1>Block: {count}</h1>
    </div>
  );
};


Then memorize our Block component using memorize function:

const MemorizedBlock = memorize(Block);


Let’s try to use the same MemorizedBlock component several times to make sure that Block was called only once.


function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <MemorizedBlock count={count}  />
      <MemorizedBlock count={count} />
      <MemorizedBlock count={count} />
    </div>
  );
}


If you check the results from our console.log, you’re going to see the result:

key [{"count":0},{}]
result {$$typeof: Symbol(react.element), type: 'div', key: null, ref: null, props: {…}, …}
key [{"count":0},{}]
key [{"count":0},{}]


You can see that usage of the MemorizedBlock was many times, but the results from all the times were the same, which means that we calculate it only once. Other iterations were from memory. The same idea React provides for memorization functionality.

React.memo

Now, let’s create more components in order to play with all of them around, and check what was updated and at which time.


export const Content = ({ value }) => {
  console.log("=== render Content ===");

  return (
    <div>
      <h1>
        Value: {value}
      </h1>
    </div>
  );
};

export const Header = ({ setCount }) => {
  console.log("=== render Header ===");

  return (
    <header>
      <h1>Header</h1>
      <button
        onClick={() => {
          setCount((count) => count + 1);
        }}
      >
        +
      </button>
      <button
        onClick={() => {
          setCount((count) => count - 1);
        }}
      >
        -
      </button>
    </header>
  );
};

export const Footer = () => {
  console.log("=== render Footer ===");

  return (
    <footer>
      <h1>Footer</h1>
    </footer>
  );
};


In each component, let's put console.log in order to see what has been rendered. Finally, summarize everything in our application:

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Header setCount={setCount} />
      <Content value={0} />
      <Footer />
    </div>
  );
}


Run the application, and you have to see in the console the following output:

=== render Header ===
=== render Content ===
=== render Footer ===


Now, if you hit on for the button, you will see that all components will be rendered. Why does it happen? Because those components (functions) actually have not been memorized.


Let’s wrap Footer to React.memo in order to store properties, and calculate only stored results from memory as it’s been described before.


export const Footer = React.memo(() => {
  console.log("=== render Footer ===");

  return (
    <footer>
      <h1>Footer</h1>
    </footer>
  );
});


And render the app again, and you can see that, after hitting the button, only two components (Header and Content) have been updated, except Footerbecause the Footer has no changes in props. Let’s do the same with the Content component.


export const Content = React.memo(({ value }) => {
  console.log("=== render Content ===");

  return (
    <div>
      <h1>Value: {value}</h1>
    </div>
  );
});


After hitting the button, you will find out that it rerenders only Header because the Header is not memorized. But as you can see, we got some value in the Content, and it’s not rerendered because the value is always the same.


<Content value={0} />


If you check the file react.development.js for memo function, there will be the function:

memo(type, compare) {
  //...
}


And comparison happens by the following condition:

compare === undefined ? null : compare


It works with twoprevProps and nextProps arguments in the additional argument for memo function like that:

export const Content = React.memo(({ value }) => {
  console.log("=== render Content ===");

  return (
    <div>
      <h1>Value: {value}</h1>
    </div>
  );
}, (prevProps, nextProps) => {
  return prevProps.value === nextProps.value;
});


Thus, you can fine-tune the props comparison for component updates. Now, add to the content the value:

<Content value={count} />


and memorize the Header component:

export const Header = React.memo(({ setCount }) => {
  console.log("=== render Header ===");

  return (
    <header>
      <h1>Header</h1>
      <button
        onClick={() => {
          setCount((count) => count + 1);
        }}
      >
        +
      </button>
      <button
        onClick={() => {
          setCount((count) => count - 1);
        }}
      >
        -
      </button>
    </header>
  );
});


After hitting the button, you find that only Content was rendered. It’s because the setCount same function from the useState and React care about it. But what if we will create our own function for the handle state, like this:

function App() {
  const [count, setCount] = useState(0);

  const setCountHandler = (isIncrement) => {
    setCount((prevCount) => (isIncrement ? prevCount + 1 : prevCount - 1));
  };

  return (
    <div>
      <Header setCount={setCountHandler} />
      <Content value={count} />
      <Footer />
    </div>
  );
}


And update the Header for function in props:

export const Header = React.memo(({ setCount }) => {
  console.log("=== render Header ===");

  return (
    <header>
      <h1>Header</h1>
      <button
        onClick={() => {
          setCount(true);
        }}
      >
        +
      </button>
      <button
        onClick={() => {
          setCount();
        }}
      >
        -
      </button>
    </header>
  );
});


By updating the state, both components Header and Content updating too. It’s because the function setCountHandler has a referential integrity issue. To have the same reference for the function, let’s go to the next part.

useCallback

For fixing the referential integrity issue, React has a special hook — useCallback. Let’s update the handler to:

const setCountHandler = useCallback((isIncrement) => {
    setCount((prevCount) => (isIncrement ? prevCount + 1 : prevCount - 1));
}, []);


Now updating only Content because it refers to the same for setCountHandler. But if it is provided to dependencies, the empty object is like this:

const dependency = {}

const setCountHandler = useCallback((isIncrement) => {
   setCount((prevCount) => (isIncrement ? prevCount + 1 : prevCount - 1));
}, [dependency]);


The problem will be back because each object has a different reference on each render. To fix that issue, let’s go to the following part.

useMemo

This hook solves the issue with values with references. Literally, it’s returned memorized value. The hook also has a dependency on updating rules.


const dependency = useMemo(() => ({}), []);

const setCountHandler = useCallback((isIncrement) => {
   setCount((prevCount) => (isIncrement ? prevCount + 1 : prevCount - 1));
}, [dependency]);


The difference between useMemo and useCallback is that useMemo returns memorized value, but useCallback returns memorized function. For example, if you provide value to Context like this:

<Content value={{ count: 0 }} />


The component will keep updating even with a static count value. In this case, useMemo also will help to solve that issue.


const value = useMemo(
  () => ({
    count: 0,
  }),
  []
);
  
// ...
  
<Content value={value} />


Now, the value is memorized, and after clicking by buttons nothing happens because the value is always the same. What happens if use state count right in useMemo hook?


const value = useMemo(
    () => ({
      count,
    }),
    []
  );


After the update, nothing’s gonna change, and the state Content will be not updated at all. It’s because the dependency useMemo has not been provided in order to update that value. To fix that, add count from state to dependency:

const value = useMemo(
  () => ({
    count,
  }),
  [count]
);


Now everything works fine. The dependency updates the useMemo correct, and value updating updates the Content component.


Conclusion

It's important to keep in mind that there is no need to memorize simple, lightweight functions. When dealing with straightforward operations that do not involve complex computations, there's little benefit in memorizing them as the cost of calculating them is minimal.


On the other hand, for more intricate functions that involve expensive calculations, it may be worthwhile to store the results to avoid unnecessary and time-consuming recalculations.


Therefore, it is recommended to exercise judgment when deciding whether to memorize a function or not, taking into account its complexity and the cost of computing it.

Resources


Also published here