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.
Top comments (0)