memoize-one vs micro-memoize vs reselect: Memoization in JavaScript (2026)
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
thiscontext binding, React patterns - micro-memoize: ~5M weekly downloads — configurable N-result cache, async support, fastest library
- reselect: ~8M weekly downloads — Redux selector memoization, composable,
createSelectorAPI - 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
| Feature | memoize-one | micro-memoize | reselect |
|---|---|---|---|
| Cache size | 1 (fixed) | Configurable | 1 (default) |
| Async support | ❌ | ✅ | ❌ |
| Custom equality | ✅ | ✅ | ✅ |
| Redux integration | ❌ | ❌ | ✅ Built for it |
| Selector composition | ❌ | ❌ | ✅ |
this binding | ✅ | ❌ | N/A |
| TypeScript | ✅ | ✅ | ✅ Excellent v5 |
| Bundle size | ~1KB | ~2KB | ~5KB |
| ESM | ✅ | ✅ | ✅ |
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
useMemoin 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.