Library / SDK
reduxjs/reselect avatar
reduxjs/reselect

Reselect memoizes twice, ships its source in the tarball, and changed its default cache in 5.0

Selector library for Redux

18,999 stars668 forksTypeScriptMIT

At a glance

What is it?
Reselect is a small TypeScript library that generates memoized selector functions for Redux, and the interesting decisions are all in the details: the result function receives extracted values rather than state, the contract is reference stability rather than saved work, version 5 changed the default memoizer to weakMapMemoize, and the published tarball includes the source directory. The README also splits installation into two paths, and Redux Toolkit users install nothing.
Who is it for?
Reselect fits a Redux codebase that stores minimal state and derives everything, that understands the reference equality contract well enough to write input selectors that return stable values, and that wants to read the implementation when a cache behaves unexpectedly, which the tarball permits. It does not fit a team that reaches for it to cache an expensive computation without regard to referential identity, because the memoization is only as good as the input selectors.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 17 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • reduxjs/reselect18,999 ★ · -13
Star history
Repository2026-09-212026-11-14
reduxjs/reselect19k19k

-13 Stars in the last 54 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Redux Toolkit users install nothing, and the import path is different

The installation section has two branches, and the first one has no command in it. While Reselect is not exclusive to Redux, it is already included by default in the official Redux Toolkit package, so no further installation is needed, and the import is:

ts
import { createSelector } from '@reduxjs/toolkit'

The standalone branch is the one with the commands:

bash
npm install reselect

with the same package named for yarn and pnpm in the block.

The consequence is that two teams on the same codebase can be running different versions of Reselect without either noticing, because the library arrives as a dependency of the toolkit rather than as a direct one. A toolkit upgrade is a Reselect upgrade, and the 5.0 changes described later in this article land in a project that never asked for them. If you want to pin the selector behaviour independently, you have to take the standalone path deliberately and accept that you now own the version.

The result function receives extracted values, not the state

The mechanism is one sentence in the README and everything about Reselect follows from it. `createSelector` accepts one or more input selectors, which extract values from arguments, and a result function that receives the extracted values and should return a derived value. If the generated output selector is called multiple times, the output will only be recalculated when the extracted values have changed.

So there are two distinct layers. The input selectors see the whole argument, in Redux the entire store state, and pull values out of it. The result function sees only what was pulled out, never the state. The visual aid in the terminology section shows the shape:

ts
const outputSelector = createSelector(
  [inputSelector1, inputSelector2, inputSelector3], // synonymous with `dependencies`.
  resultFunc // Result function
)

The comment matters as much as the code. The array of input selectors is synonymous with `dependencies`, which is the term the library uses for the same thing, and the output selector depends on them.

That is also why there are two memoization functions rather than one, `argsMemoize` and `memoize`, configurable separately. The outer layer keys on the arguments the output selector was called with, the inner on the values the input selectors returned.

The contract is reference stability, not saved work

The README's example contrasts a plain function that logs and runs on every call with a memoized version that runs once and returns the same value afterwards. That part is the expected benefit. The sentence that follows is the one to remember:

In addition to skipping unnecessary recalculations, the memoized selector returns the existing result reference if there is no recalculation, and this is important for libraries like React-Redux or React that often rely on reference equality checks to optimize UI updates.

So the guarantee is about identity, not only about time. When nothing changed, you get the same object back, and a consumer comparing with `===` sees no change and skips its update. That is a stronger and more useful promise than a speedup, and it is also the promise that is easiest to break by accident.

The failure is on the input side. If an input selector builds a new array or object on each call, the extracted value is never equal to the previous one, the inner memoization never hits, and the result function runs every time. The reference contract is only as good as the selectors feeding it, which is why 5.0 introduced `inputStabilityCheck`, a development tool that runs the input selectors twice with the same arguments and warns if they return differing results.

Version 5 changed the default memoizer to weakMapMemoize

The 5.0 notes list the change as a new default memoization function, `weakMapMemoize`, and the API reference still documents `lruMemoize` alongside it. So `lruMemoize` is still available; it is just not the default.

That is a behavioural change in the hot path rather than an addition, and it is the kind of change that shows up as a performance difference rather than an exception. The same notes also record that `memoize` and `argsMemoize` were incorporated into the output selector fields for debugging purposes, which means a selector now carries a reference to the strategy it is using and you can inspect it.

Customisation went the same way. `createSelectorCreator` accepts an options object allowing customised `memoize` and `argsMemoize` functions alongside their respective `memoizeOptions` and `argsMemoizeOptions`, and `createSelector` supports direct customisation of `memoize` and `argsMemoize` within its own options object.

The practical consequence is that a 4.x recipe is not automatically a 5.x recipe. If your code was written against the old default, the behaviour is now whatever `weakMapMemoize` does, and the fix is to pass the previous function explicitly rather than to assume nothing changed.

The TypeScript nesting limit moved from about 8 to about 30

The 5.0 notes are unusually quantitative about types, and the number is the point. Support for TypeScript below 4.7 was discontinued, aligning with modern TypeScript features, and TypeScript performance for nesting output selectors was significantly improved, with the nesting limit increased from approximately 8 to around 30 output selectors.

The stated benefit is reducing the occurrence of the infamous `Type instantiation is excessively deep and possibly infinite` error. That error is a compiler complaint about a type that recurses too far, and eight levels of composed selectors is a shallow graph by any real application standard.

Two removals come with it. The second overload of `createStructuredSelector` was removed because of its susceptibility to runtime errors, which is a rare and welcome admission that an API shape was unsafe. And the breaking changes list the removal of the `ParametricSelector` and `OutputParametricSelector` types.

For anyone migrating, the practical checklist is: move to TypeScript 4.7 or later, expect up to about 30 levels of nesting to type check, and drop the two removed parametric types.

The published tarball includes src, so the implementation is readable

One manifest field is unusual enough to notice. The `files` array is:

json
  "files": [
    "src",
    "dist"
  ],

The source directory ships alongside the build. Most libraries publish only `dist`, which is the whole point of building, and this one publishes both.

For a library whose behaviour is defined by cache semantics, that is a real advantage. When a selector returns a stale value or recomputes when you expected it not to, the answer is in `node_modules/reselect/src`, at the version you actually installed, rather than in a source map or a guess at a GitHub tag. It also means a security review of a Reselect dependency can read the code rather than the bundle.

The offset is size, and it is small. `"sideEffects": false` is set, so a bundler can still drop the import, and the `types` field points at `./dist/reselect.d.ts` rather than at the source, so type resolution is unaffected.

module and exports.import name two different ESM files

The entry point configuration has a subtlety that only shows up when you read it against a bundler.

json
  "main": "./dist/cjs/index.js",
  "module": "./dist/reselect.legacy-esm.js",
  "types": "./dist/reselect.d.ts",

So `main` is the CommonJS build and `module` is a file named `reselect.legacy-esm.js`. The `exports` map then routes the modern path elsewhere:

json
    ".": {
      "types": "./dist/reselect.d.ts",
      "import": "./dist/reselect.mjs",
      "default": "./dist/cjs/index.js"
    }

The `import` condition points at `reselect.mjs`, which is a different file from the `reselect.legacy-esm.js` that the legacy `module` field names, and the `default` condition falls back to the CommonJS build.

The consequence is that a tool that reads the `exports` map and one that reads `module` are not loading the same artefact. That is normal for a library in transition between legacy and modern ESM, and it is worth knowing when a bundle behaves differently from a direct Node import. The `"types"` condition comes first in the map, so a TypeScript resolver takes the declaration file before either implementation.

There is a heap snapshot script for weakMapMemoize and two type test suites

The remaining scripts say where the project's residual risk sits, and it is not where a feature list would point.

The default memoizer has a dedicated memory tool. `heap-snapshot` builds the package and then runs `node --expose-gc ./scripts/weakMapMemoizeHeapSnapshot.mjs`, and the hot path benchmark is the same shape: `node ./bench/build.mjs && node --expose-gc ./bench/run.mjs`. The explicit garbage collector flag is the point. A `WeakMap`-backed cache either gets collected or it does not, and the only way to know is to force collection and look.

The TypeScript side gets two separate directories, `type-tests/` and `typescript_test/`, with different commands. `test` is `vitest --run && pnpm type-check`, and `type-check` runs vitest's typecheck mode followed by a separate `tsc --noEmit` against `type-tests/tsconfig.looseNullChecks.json`. A third entry, `type-check:trace`, runs the compiler with `--generateTrace` and pipes it into a TypeScript analysis tool, which is how you find out where the type checker is actually spending its time.

The rest of the tooling is current: `tsdown` for the build with a post-step script that strips a sourcemap field, `oxfmt` and `oxlint` for formatting and linting, `release-it` for releases, a `bench/` directory with a vitest bench mode, `codecov.yml`, and `netlify.toml` with a `CNAME` for the documentation site at reselect.js.org.

Editorial conclusion

Reselect fits a Redux codebase that stores minimal state and derives everything, that understands the reference equality contract well enough to write input selectors that return stable values, and that wants to read the implementation when a cache behaves unexpectedly, which the tarball permits. It does not fit a team that reaches for it to cache an expensive computation without regard to referential identity, because the memoization is only as good as the input selectors. Before adopting, check three things: whether you already have it through Redux Toolkit, since the import path is `@reduxjs/toolkit` and the version is then decided by your toolkit version, whether the selector graph nests more than about 30 deep, which is the limit 5.0 raised from roughly 8, and whether you are on 4.x code being moved to 5.x, given the new `weakMapMemoize` default and the removal of the `ParametricSelector` and `OutputParametricSelector` types.

Frequently asked questions

What is Reselect?

It is a library for creating memoized selector functions, commonly used with Redux but usable with any plain JavaScript immutable data. Selectors compute derived data so Redux can store the minimal possible state, they are not recomputed unless one of their arguments changes, and they are composable because a selector can be the input to another.

What is a Redux selector?

The README defines a selector function as a function that accepts one or more JavaScript values as arguments and derives a result; when used with Redux, the first argument is typically the entire Redux store state. In Reselect these are the input selectors, which extract values and pass them to a result function that does the derivation.

What changed in Reselect 5.0.0?

A new default memoization function, weakMapMemoize, alongside the still documented lruMemoize; customisation of memoize and argsMemoize through options on both createSelectorCreator and createSelector; TypeScript support discontinued below 4.7; the output selector nesting limit raised from about 8 to about 30; and removals including the second createStructuredSelector overload and the ParametricSelector and OutputParametricSelector types.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. reduxjs/reselect on GitHub
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/reduxjs-reselect.svg)](https://hysenlabs.com/projects/reduxjs-reselect)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/reduxjs-reselect.svg)](https://hysenlabs.com/projects/reduxjs-reselect)

Was this useful?