mrkeyoor.com_
Thu 06 Aug 02:46 UTC
npmUtilsupdated 06 Aug 2026

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.

Verdict

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.

API stability5/5memoizeOne(fn, isEqual) has been the whole surface since 2018. The 6.0.0 major was tightened TypeScript types plus a new .clear() method, with the release notes explicitly stating no behaviour or API changes. Nothing has broken since.
Docs4/5The README is unusually honest for its size: it documents the default equality rules, the NaN special case, this handling, what happens when the function throws, and the fact that function properties and .length are not preserved. There is no hosted docs site, and the benchmark table was run on Node 16.
Maintenance2/5The last release was 6.0.0 in October 2021 and the last push to the repository was January 2023, with 2 open issues on GitHub. Zero dependencies and 70 lines of logic mean low decay risk, but planned work such as dropping IE11 and restoring the .length property has sat untouched for years.
Ecosystem5/5Around 34.7M weekly downloads, largely as a transitive dependency: react-window 1.x lists it in its dependencies, for example. No plugin ecosystem exists because there is nothing to extend, but it is a safe assumption that it is already in your lockfile.

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
Skip it if

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 again

Call 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 object

The 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 hit

The 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 survived

Failed 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 added

This 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

PackageRegistryPick it when
moizenpmYou need more than one cache entry, plus maxAge, maxSize, promise handling, and React component memoization in one package.
nano-memoizenpmYou want memoize-one's size profile but with a real multi-entry cache and faster single-argument paths.
async-memoize-onenpmThe function you are memoizing returns a promise and you want rejections to bust the cache instead of sticking.
reselectnpmYou are composing derived state in Redux and want selectors that chain, with per-instance factories built in.