loading...

What is function Memoization and why should you care?

alexgurr profile image Alex Gurr ・3 min read

Memoization is a general software engineering principal/idealogy that can be applied to code in any language. My examples and libraries will all be JavaScript.

So What is Memoization?

Memoization is the principal of caching the result of a function call. If you call a function multiple times with the same arguments, you'll get the cached result every time. The logic in your function will not re-run when there's a cached result.

Why/When Would I Ever Need This?

Memoization is great when you find functions being called over and over again (such as in a render call in React). Your function might have some complex logic that your performance would benefit from by not calling the same logic over and over.

tl;dr performance for functions called multiple times with the same arguments.

Memoization in React

The concept of Memoization in React is exactly the same. We want to cache the result of a function call. Except in this scenario, our function returns JSX and our arguments are props.

If you have a parent being re-rendered, your child function will be called on every render, even if the props don't change. React provides us a React.memo utility and a useMemo hook which we can utilise in our functional components to prevent unnecessary re-renders.

We can also utilise normal Memoization in class methods and other JS functions in our react components. A traditional pattern in React class components was to react to prop changes through componentWillReceiveProps, apply some logic to a prop and set it in state. Now that componentWillReceiveProps is on the way to being deprecated, Memoization provides us a great alternative method to achieving the same result. See the examples section below.

https://reactjs.org/docs/react-api.html#reactmemo

Some vanilla JS memoization Libraries

For general JavaScript, I'd recommend two battle-tested libraries instead of trying to implement yourself, which I've covered below.

Lodash.memoize

Creates a memoization results map, meaning it will effectively store the history of all results for use in the future.

Serialises only the first argument to string. Be careful about passing objects. Multiple arguments aren't compared.

Useful if you're calling the function from multiple places with different arguments.

https://lodash.com/docs/4.17.15#memoize

Memoize One

Stores the last result of the function call. Will only ever compare the arguments to the last ones the function was called with.

Uses all the arguments for comparison between function calls. No serialisation of objects so you can pass anything.

Useful if you're only calling the memoized function from one place.

https://github.com/alexreardon/memoize-one

Differences Between the Two

  • Lodash memoize will serialize the arguments to use as a map key
  • Lodash memoize will only use the first argument
  • Memoize One will only remember the set of arguments/result of the previous function call. Lodash memoize will maintain a result map.

How About Some Examples?

A normal function

import _memoize from 'lodash.memoize';
import memoizeOne from 'memoize-one';

const myFunc = users => users.filter(user => user.gender === 'female');

const myMemoizedFunc = _memoize(user => users.filter(user => user.gender === 'female'));

const myMemoizedOnceFunc = memoizeOne(user => users.filter(user => user.gender === 'female'));

React.memo

import React, { memo } from 'react';

function MyFunctionalComponent {
  return <div />;
}

export default memo(MyFunctionalComponent);

Before/After, React class component real world scenario

Before

import React, { Component } from 'react';

function filterUsers(users) {
  return users.filter(({ gender }) => gender === 'female');
}

export default class FemaleUserList extends Component {
  constructor(props) {
    super(props);

    const { allUsers } = props;

    this.state = {
      femaleUsers: filterUsers(allUsers)
    }
  }

  componentWillReceiveProps(nextProps) {
    const { allUsers } = nextProps;

    if (allUsers !== this.props.allUsers) {
      this.setState({
        femaleUsers: filterUsers(allUsers)
      });
    }
  }

  render() {
    const { femaleUsers } = this.state;

    return femaleUsers.map(User);
  }  
}

After

import React, { Component } from 'react';
import memoizeOne from 'memoize-one';

export default class FemaleUserList extends Component {
  // We bind this function to the class now because the cached results are scoped to this class instance
  filterUsers = memoizeOne(users => users.filter(({ gender }) => gender === 'female'));

  render() {
    const { allUsers  } = this.props;
    const femaleUsers = this.filterUsers(allUsers);

    return femaleUsers.map(User);
  }
}

A React form

import React, { Component } from 'react';
import _memoize from 'lodash.memoize';

export default class FemaleUserList extends Component {
  // Yes, we can even return cached functions! This means we don't have to
  // keep creating new anonymous functions
  handleFieldChange = _memoize((fieldName) => ({ target: { value } }) => {
    this.setState({ [fieldName]: value });
  }); 


  render() {
    const { email, password } = this.state;

    return (
      <div>
        <input onChange={this.handleFieldChange('email')} value={email} />
        <input
          onChange={this.handleFieldChange('password')}
          value={password}
          type="password"
        />
      </div>
    );
  }
}

Closing Words

Memoization is a great tool in a developer's arsenal. When used correctly and in the right places, it can provide great performance improvements.

Just be aware of the gotchas, especially when using React.memo and expecting things to re-render.

Posted on by:

alexgurr profile

Alex Gurr

@alexgurr

Full stack developer 🚀. Passion for fitness, food, and building JavaScript thingymabobs ♥️. Head of Tech @ Shootsta

Discussion

markdown guide