Skip to main content

Guide

memoize-one vs micro-memoize vs reselect 2026

Compare memoize-one, micro-memoize, and reselect for memoizing functions in JavaScript. Cache sizes, equality checks, Redux selectors, React patterns.

·PkgPulse Team·
0

TL;DR

memoize-one caches only the most recent call — perfect for React components where the same function is called repeatedly with the same recent arguments. micro-memoize is the most flexible and fastest general-purpose memoizer — configurable cache size, custom equality, and supports async functions. reselect is the Redux selector memoization library — composes input selectors, memoizes the result selector, and is used across the Redux ecosystem. For React component methods and derived values: memoize-one. For general function memoization with cache size control: micro-memoize. For Redux/Zustand derived state: reselect.

Key Takeaways

  • memoize-one: ~10M weekly downloads — 1-result cache, correct this context binding, React patterns
  • micro-memoize: ~5M weekly downloads — configurable N-result cache, async support, fastest library
  • reselect: ~8M weekly downloads — Redux selector memoization, composable, createSelector API
  • memoize-one clears cache on every new argument set — never leaks memory
  • micro-memoize's default cache size is 1 (like memoize-one) but configurable up to Infinity
  • useMemo (React) often replaces these in component-level code — libraries for other uses

The Memoization Problem

// Without memoization — recalculates every render even if input is the same:
function getFilteredPackages(packages: Package[], minScore: number) {
  return packages.filter((p) => p.healthScore >= minScore)
    .sort((a, b) => b.healthScore - a.healthScore)
}

// Called on every render — if packages and minScore haven't changed, wasted work:
const filtered = getFilteredPackages(allPackages, 80)

// With memoization — returns cached result if inputs are the same:
import { memoizeOne } from "memoize-one"

const memoizedFilter = memoizeOne(getFilteredPackages)
memoizedFilter(allPackages, 80)  // Runs filter
memoizedFilter(allPackages, 80)  // Returns cached result (same args)
memoizedFilter(allPackages, 70)  // Runs again (different minScore)
memoizedFilter(allPackages, 70)  // Returns cached result

memoize-one

memoize-one — single-result cache memoization:

Basic usage

import { memoizeOne } from "memoize-one"

// Memoize any function:
function formatPackageData(packages: Package[], locale: string) {
  console.log("Recalculating...")
  return packages.map((p) => ({
    ...p,
    formattedDownloads: new Intl.NumberFormat(locale).format(p.downloads),
  }))
}

const memoizedFormat = memoizeOne(formatPackageData)

const result1 = memoizedFormat(packages, "en-US")  // "Recalculating..."
const result2 = memoizedFormat(packages, "en-US")  // No log — cached!
const result3 = memoizedFormat(packages, "de-DE")  // "Recalculating..." (new locale)
const result4 = memoizedFormat(packages, "en-US")  // "Recalculating..." (prev was de-DE)

// memoize-one only caches the LAST call's result
// This makes it memory-safe — old results are GC'd

React class component method

import { memoizeOne } from "memoize-one"
import { Component } from "react"

interface Props {
  packages: Package[]
  minScore: number
}

class PackageList extends Component<Props> {
  // Memoize expensive filter — recalculates only when props change:
  getFilteredPackages = memoizeOne(
    (packages: Package[], minScore: number) =>
      packages
        .filter((p) => p.healthScore >= minScore)
        .sort((a, b) => b.healthScore - a.healthScore)
  )

  render() {
    const { packages, minScore } = this.props
    const filtered = this.getFilteredPackages(packages, minScore)

    return <ul>{filtered.map((p) => <PackageCard key={p.name} package={p} />)}</ul>
  }
}

// This pattern is equivalent to useMemo in function components:
// const filtered = useMemo(() => ..., [packages, minScore])

Custom equality check

import { memoizeOne } from "memoize-one"

// Default: strict equality (===)
// Custom: deep equality for object/array args:
import { isEqual } from "lodash"

const memoizedFn = memoizeOne(
  (filter: { minScore: number; tags: string[] }) =>
    computeExpensiveResult(filter),
  isEqual  // Use deep equality — { minScore: 80, tags: ["ui"] } matches again
)

// With deep equality:
memoizedFn({ minScore: 80, tags: ["ui"] })  // Computed
memoizedFn({ minScore: 80, tags: ["ui"] })  // Cached! (deep equal)
memoizedFn({ minScore: 80, tags: ["react"] })  // Recomputed

// Warning: isEqual is expensive — use it only when shallow comparison isn't enough

micro-memoize

micro-memoize — fast, configurable memoization:

Basic usage

import memoize from "micro-memoize"

// Default: cache size of 1 (same as memoize-one), strict equality:
const memoizedFn = memoize((a: number, b: number) => {
  console.log("Computing...")
  return a + b
})

memoizedFn(1, 2)  // "Computing..." → 3
memoizedFn(1, 2)  // Cached → 3
memoizedFn(2, 3)  // "Computing..." → 5
memoizedFn(1, 2)  // "Computing..." (cache evicted by 2,3)

Configurable cache size

import memoize from "micro-memoize"

// Cache last 10 calls (LRU-like eviction):
const memoizedFormatter = memoize(
  (value: number, currency: string) =>
    new Intl.NumberFormat("en-US", { style: "currency", currency }).format(value),
  { maxSize: 10 }
)

// Useful when function is called with a known set of arguments:
// e.g., 10 packages × currency — all cached:
memoizedFormatter(1999, "USD")  // "$19.99"
memoizedFormatter(1999, "EUR")  // "€19.99"
memoizedFormatter(2499, "USD")  // "$24.99"
// All 3 cached — different (value, currency) pairs

Async memoization

import memoize from "micro-memoize"

// Memoize async functions:
const fetchPackageData = memoize(
  async (packageName: string) => {
    console.log(`Fetching ${packageName}...`)
    const res = await fetch(`https://registry.npmjs.org/${packageName}`)
    return res.json()
  },
  {
    maxSize: 50,   // Cache 50 packages
    isPromise: true,  // Store the promise — parallel calls for same key share one promise
  }
)

// Multiple parallel calls for "react" — only ONE fetch:
const [data1, data2] = await Promise.all([
  fetchPackageData("react"),
  fetchPackageData("react"),  // Returns same promise as above
])
// Only one "Fetching react..." logged

Custom equality

import memoize from "micro-memoize"

// Custom equality for complex objects:
const memoizedChart = memoize(
  (config: ChartConfig) => generateChart(config),
  {
    isEqual: (a, b) =>
      a.type === b.type &&
      a.data.length === b.data.length &&
      a.data.every((v, i) => v === b.data[i]),
    maxSize: 5,
  }
)

reselect

reselect — Redux selector memoization:

Basic selector

import { createSelector } from "reselect"

// Input selectors (extract data from state):
const selectPackages = (state: RootState) => state.packages.items
const selectFilter = (state: RootState) => state.packages.filter

// Result selector (memoized — only recomputes when inputs change):
const selectFilteredPackages = createSelector(
  [selectPackages, selectFilter],
  (packages, filter) => {
    console.log("Recomputing filtered packages...")
    return packages
      .filter((p) => p.healthScore >= filter.minScore)
      .filter((p) => !filter.tags.length || filter.tags.some((t) => p.tags.includes(t)))
      .sort((a, b) => b.healthScore - a.healthScore)
  }
)

// Usage in component:
import { useSelector } from "react-redux"

function PackageList() {
  // Will only re-render if selectFilteredPackages returns a new reference:
  const packages = useSelector(selectFilteredPackages)
  return <ul>{packages.map((p) => <PackageCard key={p.name} package={p} />)}</ul>
}

Composing selectors

import { createSelector } from "reselect"

// Build complex selectors from simpler ones:
const selectAllPackages = (state: RootState) => state.packages.items
const selectSearchQuery = (state: RootState) => state.search.query
const selectSortField = (state: RootState) => state.ui.sortField

// Compose:
const selectSearchResults = createSelector(
  [selectAllPackages, selectSearchQuery],
  (packages, query) =>
    query ? packages.filter((p) => p.name.includes(query.toLowerCase())) : packages
)

const selectSortedSearchResults = createSelector(
  [selectSearchResults, selectSortField],
  (packages, sortField) => [...packages].sort((a, b) => b[sortField] - a[sortField])
)

// Each selector memoizes independently — reselect only recomputes when its
// specific inputs change:
// selectSearchResults recomputes when packages or query changes
// selectSortedSearchResults recomputes when searchResults or sortField changes

reselect v5 (2024) — improved TypeScript

import { createSelector } from "reselect"

// reselect v5 has much better TypeScript inference:
const selectActivePackages = createSelector(
  [(state: RootState) => state.packages, (state: RootState) => state.filter.minScore],
  (packages, minScore) => packages.filter((p) => p.healthScore >= minScore)
)
// Return type inferred as Package[] — no explicit types needed

Feature Comparison

Featurememoize-onemicro-memoizereselect
Cache size1 (fixed)Configurable1 (default)
Async support
Custom equality
Redux integration✅ Built for it
Selector composition
this bindingN/A
TypeScript✅ Excellent v5
Bundle size~1KB~2KB~5KB
ESM

Understanding the Memoization Trade-off

Memoization trades memory for computation time. The correct choice between these libraries depends on understanding your function's call patterns: how many unique argument combinations does your function receive in practice, and how expensive is each computation? A formatting function called with dozens of different locales benefits from micro-memoize's configurable cache size, where maxSize: 20 holds the twenty most recently used locale combinations without unbounded growth. A derived data computation in React where the parent component re-renders frequently with the same props benefits from memoize-one's single-result cache — the function runs once and returns cached results for all subsequent renders with the same inputs. The worst outcome is applying the wrong tool: using memoize-one for a function called with many different argument combinations produces no cache benefit, while using micro-memoize with maxSize: Infinity for a long-lived process accumulates memory indefinitely.

Production Considerations and Cache Invalidation

Memoization in production code introduces subtle bugs when the cache outlives its useful lifetime. Memoize-one's single-result design sidesteps most of these issues — the cache clears on every new argument set, preventing stale data from accumulating. Micro-memoize with a large maxSize can hold onto expensive objects longer than intended, especially in server-side rendering contexts where a memoized function is defined at module scope and shared across requests — each request adds to the cache but never triggers eviction. For SSR workloads, prefer function-level memoization within request handlers rather than module-scope memoize calls, or use maxSize: 1 to mirror memoize-one's behavior with micro-memoize's additional capabilities like async support.

Reselect and Derived State Performance

The performance win from reselect in a Redux application is compounding rather than linear. A selector that recomputes on every state update triggers re-renders in every subscribed component, which in turn triggers their child renders. A properly memoized selector chain means that a deeply nested state update only propagates re-renders to components whose actual inputs changed. Reselect v5's createSelector tracks how many times each selector recomputes in development via the resultEqualityCheck option — enabling this during development reveals selectors that recompute more often than expected, which typically indicates that an upstream selector is returning a new array or object reference on every call despite containing the same data.

TypeScript Integration

Reselect v5 shipped a ground-up TypeScript rewrite that eliminated the need for manual return type annotations. The createSelector function infers the output type from the result function's return type, and the input selectors' return types are correctly distributed to the result function's parameter list. This inference works through up to twelve input selectors before hitting TypeScript's inference limits. Micro-memoize's TypeScript support is functional but requires explicit generic parameters when the function signature is complex — memoize<(filter: FilterState) => Package[]>(fn, options) is typically cleaner than relying on inference. For memoize-one, the isEqual comparator receives typed arguments matching the memoized function's parameter types, enabling type-safe custom equality implementations.

Memory Safety and Async Edge Cases

Micro-memoize's isPromise: true option deduplicates concurrent calls for the same cache key, but it does not handle promise rejection correctly in some configurations — a rejected promise remains in the cache until the next call with the same arguments evicts it, causing all callers to receive the cached rejection. Wrap async memoized functions to clear the cache entry on rejection using memoizedFn.cache.delete(key). Reselect does not support async selectors directly — use RTK Query's createSelector integration or a library like re-reselect for parameterized selectors that need per-ID caching, rather than creating a new reselect instance per component (which defeats the memoization entirely).

Community Adoption and Alternatives

Memoize-one remains the most downloaded memoization library despite React's useMemo hook handling most in-component use cases. Its continued popularity reflects two patterns: class component codebases that haven't migrated to hooks, and server-side or non-React JavaScript where useMemo isn't available. Reselect's trajectory is tied entirely to Redux's: RTK's createSlice encourages selector co-location, and RTK Query's cache layer reduces the need for manual selector memoization in data-fetching scenarios. If your team is adopting Zustand or Jotai, micro-memoize's configurable cache size makes it a better fit than reselect, since those state managers don't have the Redux selector composition model.

Memoization in Server Components and RSC Patterns

React Server Components introduce new considerations for memoization. Because RSC rendering happens on the server and each request creates a fresh component tree, instance-level memoization via useMemo and useCallback doesn't apply — those hooks are unavailable in server components. memoize-one and micro-memoize can still be used in Server Components for expensive computations shared across multiple renders of the same component within a single request, but the cache is scoped to the server process rather than to individual user sessions. React's cache() function (available in React 19) is the preferred tool for memoizing per-request data fetches in RSC — it deduplicates identical fetch calls within a single server render. For reselect, Redux selectors are typically used in client components where the Redux store lives, making RSC integration straightforward: keep selectors in client-side hooks and use React 19's use() for server-side data fetching instead of selector-based derivation.

When to Use Each

Choose memoize-one if:

  • React class component methods that recalculate on render
  • Any function where you only need to cache the most recent call
  • When memory safety is paramount — single cached result, no growth
  • Replacing useMemo in non-React code (Svelte, Vue, plain JS)

Choose micro-memoize if:

  • Need configurable cache size (cache N recent calls)
  • Async function memoization with promise deduplication
  • Multiple argument combinations need to be cached simultaneously
  • Fastest raw memoization performance

Choose reselect if:

  • Redux or Zustand derived state
  • Composing multiple input selectors into a computed value
  • Preventing unnecessary re-renders from state selection
  • RTK Query cache + selector composition

Use React's built-ins when in React:

// For component-level memoization, React's built-ins are usually sufficient:

// useMemo — memoize computed values:
const filtered = useMemo(
  () => packages.filter((p) => p.score >= minScore),
  [packages, minScore]
)

// useCallback — memoize function references:
const handleSelect = useCallback(
  (id: string) => dispatch(selectPackage(id)),
  [dispatch]
)

// React.memo — memoize component renders:
const PackageCard = React.memo(({ package: pkg }: { package: Package }) => (
  <div>{pkg.name}</div>
))

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on memoize-one v6.x, micro-memoize v4.x, and reselect v5.x.

Compare React and utility packages on PkgPulse →

See also: React vs Vue and React vs Svelte, culori vs chroma-js vs tinycolor2.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.