Memoization in JavaScript and React

ยท

6 min read

Introduction

Memoization is an interesting concept, and I believe all Javascript developers should be fascinated by it and familiar with it.

I will be going through this subject in the following way: WHAT, WHY and HOW

1. What is Memoization?

When I first saw the word memoization, what came to mind was memorize, and I was confused! How is JavaScript supposed to memorize and remember something on my behalf? (I was wondering if there is some form of machine learning in JavaScript by default.) But going deep into the concept of memoization, I understood that it is all about helping JavaScript memorize previous computations.

In other words, memoization is an optimization technique that helps to speed up repetitive, expensive computations by remembering the result of the previous computation.

2. Why Memoization?

The technique revolves around making computation efficient and faster. If an expensive computation is done with the Memoization technique, the result can be stored in, e.g., a cache and retrieved when it's needed. Hence, there's no need to recompute.

You have all this with memoization, and in addition, you get efficient computation, optimization, and faster computation (since it skips what has been done before).

3. How to implement Memoization?

In JavaScript?

Implementing memoization is about passing a function to a memoized callback

const multiplyBy10 = (num: number) => num * 10;
console.log('Simple call', multiplyBy10(3));

/**
 * 
 * Explanation: a simple memoize function that takes in a function
 * 
 * @param fn a function to be memoized or used to perform computation
 * @returns a memoized function
 */

const memoize = (fn: Function) => {
  let cache = {};

  return (...args) => {
    let num = args[0];  // just taking one argument here

    if (num in cache) {
      console.log('Fetching from cache');
      return cache[num];
    } else {
      console.log('Calculating result');
      let result = fn(num);
      cache[num] = result;
      return result;
    }
  }

}

// creating a memoized function for the 'multiplyBy10' pure function

const memoizedAdd = memoize(multiplyBy10);
console.log(memoizedAdd(3));  // calculated
console.log(memoizedAdd(3));  // cached
console.log(memoizedAdd(4));  // calculated
console.log(memoizedAdd(4));  // cached

thanks to Codegrepper and Agreeable Armadillo for the code reference

Screenshot 2022-07-14 at 14.55.54.png

Thanks to Denigma for the explanation; you can read more about it here

In React

There are several ways of implementing memoization, and this is based on what needs to be done

  1. for component use React.memo()

  2. If you want to memoize a function, then use React.useCallback();

  3. If you want to memoize the result of an expensive function, then use React.useMemo();

These methods prevent unnecessary re-rendering in React if used the right way

Understanding React.memo()

/**
 * Explanation: 
 *  this function accept a name and render a styled version of it
 * 
 * @param name
 * @returns JSX.Element (styled version of the name)
 **/
import React from 'react';

function RenderName({ name }: string) {
    return <span className="text-red-500">{name}</span>
}

export default React.memo(RenderName);

Consider the component above(child-component) without the React.memo() higher order component (HOC) we'll have issues with re-rendering when the name passed to the RenderName component are the same; but React.memo() HOC helps to prevent this unnecessary re-renders

NB: memo does not optimize a function being passed to the child component; that's why we need React.useCallback()

Understanding React.useCallback()

/**
 * Explanation:
 * a password field that handles users password 
 * and provides a toggle for the password
 *  
 * @returns JSX.Element (a styled password field)
 */
import React from 'react';
import eye from 'images/eye.svg';
import eyeClose from 'images/eye-close.svg';

function PasswordInput() {
  const [show, setShow] = React.useState<boolean>(false);
  const toggleSecret = React.useCallback(() => setShow(x => !x), []);

  return (
    <div className="h-8 flex items-center">
      <input type="password" className="..." placeholder="Enter Password" />

      <button onClick={toggleSecret}>
        <img src={show ? eyeClose : eye} alt="toggle secret" />
      </button>
    </div>
  );
}

export default PasswordInput;

with React.useCallback() the toggleSecret() function is memoized and whenever you click this the toggle button it doesn't cause a re-render there by increasing optimization. Note: we passed the arrow function to the React.useCallback(). the arrow function is: () => setShow((x) => !x) React.useCallback() also helps to prevent re-rendering when a function is being passed down to a child component, This is done by 'keeping' the function passed to the child component while the parent component re-render.

Understanding React.useMemo()

/**
 * Explanation:
 * The code demonstrates how to create a DynamicTable using React's useMemo() function.
 * The DynamicTable component is a wrapper around the Table component.
 * The DynamicTable component is responsible for creating the columns and data for the Table component.
 * 
 * @param {values: Record<string, string>[] | null}
 * @returns returns a JSX.Element(Table)
 */

import React, { useMemo } from 'react';
import Table, { ColumnsProps } from './Table';

interface DynamicTableType {
  values: Record<string, string>[] | null;
}

const DynamicTable = ({ values }: DynamicTableType): JSX.Element => {
  const columns: ColumnsProps[] = useMemo(() => {
    if (!values) return [];
    const keys = Object.keys(values?.[0]);

    const result = [];

    for (let index = 0; index < keys.length; index++) {
      const element = keys[index];
      result.push({
        Header: element?.replace('_', ' '),
        accessor: element,
      });
    }

    return result;
  }, [values]);

  const data: Record<string, string>[] = useMemo(() => {
    if (!values) return [];
    const result: Record<string, string>[] = [];

    for (let index = 0; index < values.length; index++) {
      const element = values[index];
      result.push(element);
    }
    return result;
  }, [values]);

  return <Table columns={columns} data={data} showSearch={false} />;
};

export default DynamicTable;

Extract from an open-source project I'm currently working on; check it out on GitHub

The whole idea behind React.useMemo() is similar to that of React.useCallback(); but in React.useMemo() the result returned from the function calculation is being cached, so you dont have to repeat such expensive calculation (provided that your dependent array of the useMemo does not change). In the above example, columns and data are the memoized value.

Conclusion

All in all, Optimization is something we should care about as engineers, and simple techniques like caching can help us prevent re-rendering and optimization issues, etc. Memoization is only needed when you are handling expensive calculations.

I am available for technical talks on data-structure, algorithms, javascript, react and react-native; you can reach out to me via twitter or email

I will also start a youtube channel soon to highlight my jouney with data-structure and algorithms, In the channel, I will be solving some leetcode questions and anyother interview materials I lay my hands on. watch out ๐Ÿ˜Š

Footnotes

Thanks for checking out this tutorial. (please like and add your comments.) You can also check out my other write-ups on my blog

If you have any questions, feedback, or comments, please let me know.

You can connect with me on twitter, email and github

You can also reach out to me (I do React-Native and ReactJS)

ย