memoize-one
memoize-one wraps a function so it remembers exactly one result: the one produced by the most recent set of arguments. Call it again with the same arguments and you get the cached value back without running the function. Call it with anything different and the old result is thrown away and replaced. That is the whole library. There is no maxSize, no maxAge, no key serializer, and no eviction policy to tune, which is the point: a one-entry cache cannot grow without bound and cannot leak. Arguments are compared with a fast shallow check (same count, each argument === the previous one, with NaN treated as equal to NaN), and you can swap in your own comparison function. It has no dependencies and ships around 400 bytes gzipped.
The right tool for exactly one job: caching the latest derived value in class components and module-level helpers, at a size where the bundle cost is a rounding error. Check your call pattern first, because a one-entry cache with alternating arguments is slower than no cache at all.
Use it if
- You are deriving something expensive from props or state outside of a React function component: a sorted list, a filtered array, a formatted date table. Class components and plain modules have no useMemo, and this is the standard substitute
- You want a memoized value with a stable reference so downstream === checks and React.memo comparisons keep passing between renders that did not change the inputs
- You are nervous about memory in a long-lived process and want a cache that provably holds at most one entry and one result, instead of an LRU you have to size correctly
- You need a memoizer that respects the this binding, which matters when you memoize a method that reads instance state rather than only its arguments
- Your call pattern alternates between argument sets. A cache of one means calling memoized(a) then memoized(b) then memoized(a) recomputes every single time, and you get a zero percent hit rate plus the equality-check overhead on top. Anything with more than one live input needs moize, nano-memoize, or a real LRU
- You are inside a React function component. useMemo and useCallback already do this per component instance and integrate with the render lifecycle; memoize-one at module scope is shared by every instance of the component, so two mounted copies with different props thrash each other's cache
- Your function returns a promise. memoize-one caches whatever the function returns, including a promise that later rejects, so a failed request stays cached until the arguments change. The README points at async-memoize-one for this reason
- You pass fresh object or array literals on each call. The default equality check is ===, so {id: 1} never matches the previous {id: 1} and nothing is ever cached until you supply a deep equality function, which costs more than the work you are trying to avoid for cheap functions
- You need the maintenance signal. 6.0.0 shipped in October 2021 and the repository was last pushed in January 2023, with 2 open issues. The code is small, dependency-free, and finished, but nobody is landing fixes, and the promised removal of IE11 support and the .length fix never happened
Setup reality
npm install memoize-one and you are done: zero dependencies, TypeScript and Flow types bundled, sideEffects false so bundlers can drop it entirely if unused. Two import details bite people. It is a default export, not a named one, so import memoizeOne from 'memoize-one' is correct and import {memoizeOne} works only by accident of your bundler; the named export briefly existed in 5.2.0 and was reverted in 5.2.1 for breaking CommonJS. There is no exports map in package.json, only main and module fields, so Node's native ESM loader resolves the CommonJS build and gives you the default via interop. Beyond that the setup work is deciding where the memoized function lives. Create it once, outside render and outside the constructor's hot path, because creating a new memoized wrapper per call gives you an empty cache every time. In class components that means an instance field; at module scope it means the cache is global to the module and shared by everyone importing it.
Patterns
Wrap a function once, call it many timesmemoize-a-function
import memoizeOne from 'memoize-one';
function expensiveSort(items, key) {
return [...items].sort((a, b) => a[key].localeCompare(b[key]));
}
const sortItems = memoizeOne(expensiveSort);
sortItems(users, 'name'); // runs expensiveSort
sortItems(users, 'name'); // cache hit, same array reference back
sortItems(users, 'email'); // arguments changed, runs againCall memoizeOne once and keep the returned function. Wrapping inside a render or a loop creates a fresh empty cache on every call, which is the single most common way people get zero cache hits.
Know when a one-entry cache does nothing for youunderstand-cache-of-one
const memoized = memoizeOne(compute);
memoized('a'); // miss, runs
memoized('b'); // miss, runs, 'a' result discarded
memoized('a'); // miss again, runs
// Alternating inputs = 0% hit rate + equality-check overhead.
// Use moize or nano-memoize when more than one input is live.This is the library's defining limitation, not a bug. Measure your actual call sequence before assuming a memoizer helps; interleaved arguments make memoize-one strictly slower than calling the function directly.
Use deep equality when arguments are fresh objectscustom-equality-function
import memoizeOne from 'memoize-one';
import isDeepEqual from 'lodash.isequal';
const format = memoizeOne(
(config) => buildFormatter(config),
isDeepEqual,
);
format({locale: 'en', currency: 'USD'});
format({locale: 'en', currency: 'USD'}); // cache hit despite a new objectThe equality function receives the whole arguments array on both sides, so newArgs === lastArgs is always false and you must compare element by element. Deep equality is not free: only use it when the wrapped function costs meaningfully more than the comparison.
Derive props in a class component without recomputingclass-component-derived-state
import React from 'react';
import memoizeOne from 'memoize-one';
class UserTable extends React.Component {
// instance field: one cache per mounted component
filterUsers = memoizeOne((users, query) =>
users.filter((u) => u.name.includes(query)),
);
render() {
const visible = this.filterUsers(this.props.users, this.props.query);
return <List rows={visible} />;
}
}Put the memoized function on the instance, not at module scope. A module-level cache is shared by every mounted UserTable, so two tables with different queries invalidate each other on every render.
Keep a stable reference so React.memo children skip re-renderstable-reference-for-react-memo
const buildColumns = memoizeOne((locale) => [
{id: 'name', label: t('name', locale)},
{id: 'email', label: t('email', locale)},
]);
// <Grid columns={...} /> is wrapped in React.memo; the array identity
// only changes when `locale` changes, so Grid stops re-rendering.
<Grid columns={buildColumns(locale)} />The value of memoization here is reference stability, not CPU time. Building two objects is cheap; handing React a new array every render is what costs you.
Release the cached result explicitlyclear-the-cache
const load = memoizeOne(buildLookupTable);
load(bigDataset); // result held in memory until arguments change
load.clear(); // drop the cached args and result now
load(bigDataset); // runs buildLookupTable again.clear() arrived in 6.0.0. Reach for it when the cached result holds a large object alive longer than you want, or in tests where a stale cache leaks between cases.
Understand what happens with async functionshandle-promises
const fetchUser = memoizeOne((id) => fetch(`/api/users/${id}`).then((r) => r.json()));
await fetchUser(1); // network call
await fetchUser(1); // same promise returned, no second call
// If the promise REJECTS, the rejected promise stays cached:
// every later fetchUser(1) re-throws the same error until `id` changes.memoize-one only skips caching when the wrapped function throws synchronously. An async function returns normally and then rejects, so the failure is cached. Either call .clear() in a catch block or use async-memoize-one.
Changing this busts the cachethis-context-is-an-argument
function getA() {
return this.a;
}
const memoized = memoizeOne(getA);
memoized.call({a: 20}); // 20
memoized.call({a: 30}); // 30, recomputed: `this` changed
memoized.call({a: 30}); // cache hitThe comparison is on the this value itself, by reference. Two structurally identical context objects count as different, and your custom equality function is not consulted for this at all.
A throw does not poison the existing cachethrowing-functions
const parse = memoizeOne((input) => {
if (!input) throw new Error('empty');
return JSON.parse(input);
});
const ok = parse('{"a":1}');
try { parse(''); } catch {} // throws, nothing cached for ''
const again = parse('{"a":1}');
ok === again; // true, the good entry survivedFailed calls are never cached, so a repeated bad input runs the function every time. That is usually what you want, but it means a hot path that throws gets no protection from memoization.
Type a custom equality function correctlytypescript-equality-typing
import memoizeOne from 'memoize-one';
import type {EqualityFn, MemoizedFn} from 'memoize-one';
function add(first: number, second: number): number {
return first + second;
}
const isEqual: EqualityFn<typeof add> = (newArgs, lastArgs) =>
newArgs[0] === lastArgs[0] && newArgs[1] === lastArgs[1];
const memoized: MemoizedFn<typeof add> = memoizeOne(add, isEqual);EqualityFn is generic over the memoized function, so the arguments arrays are typed as Parameters<typeof add>. Passing (a: any[], b: any[]) still compiles but gives up the checking you installed the types for.
Expect properties and length to disappearfunction-properties-lost
function add(a, b) { return a + b; }
add.displayName = 'add';
const memoized = memoizeOne(add);
memoized.displayName; // undefined
memoized.length; // 0, not 2
memoized.clear; // the only property that is addedThis trips up code that inspects fn.length (some curry and dependency-injection helpers do) or reads displayName for debugging. The typings model it correctly, so TypeScript catches it; plain JavaScript will not.
Import the default export, not a named oneimport-style
// correct
import memoizeOne from 'memoize-one';
const memoizeOne = require('memoize-one');
// wrong: the named export was added in 5.2.0 and reverted in 5.2.1
import {memoizeOne} from 'memoize-one';package.json has main and module but no exports map, so Node's ESM loader takes the CommonJS build and hands you the default through interop. Bundlers pick the ESM build via the module field.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| moize | npm | You need more than one cache entry, plus maxAge, maxSize, promise handling, and React component memoization in one package. |
| nano-memoize | npm | You want memoize-one's size profile but with a real multi-entry cache and faster single-argument paths. |
| async-memoize-one | npm | The function you are memoizing returns a promise and you want rejections to bust the cache instead of sticking. |
| reselect | npm | You are composing derived state in Redux and want selectors that chain, with per-instance factories built in. |