├── .codesandbox └── ci.json ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ ├── build-and-test-types.yml │ └── publish.yml ├── .gitignore ├── .prettierrc.json ├── .release-it.json ├── .yarn └── releases │ └── yarn-4.4.1.cjs ├── .yarnrc.yml ├── AUTHORS ├── CHANGELOG.md ├── CNAME ├── CREDITS.md ├── LICENSE ├── README.md ├── codecov.yml ├── docs └── examples │ ├── FAQ │ ├── MyComponent.tsx │ ├── createCurriedSelector.ts │ ├── createParametricSelectorHook.ts │ ├── currySelector.ts │ ├── howToTest.test.ts │ ├── identity.ts │ └── selectorRecomputing.ts │ ├── basicUsage.ts │ ├── createSelector │ ├── annotateResultFunction.ts │ ├── createAppSelector.ts │ └── withTypes.ts │ ├── createStructuredSelector │ ├── MyComponent.tsx │ ├── modernUseCase.ts │ └── withTypes.ts │ ├── development-only-stability-checks │ ├── identityFunctionCheck.ts │ └── inputStabilityCheck.ts │ ├── handling-empty-array-results │ ├── fallbackToEmptyArray.ts │ └── firstPattern.ts │ ├── lruMemoize │ ├── referenceEqualityCheck.ts │ ├── usingWithCreateSelector.ts │ └── usingWithCreateSelectorCreator.ts │ ├── tsconfig.json │ ├── unstable_autotrackMemoize │ ├── usingWithCreateSelector.ts │ └── usingWithCreateSelectorCreator.ts │ └── weakMapMemoize │ ├── cacheSizeProblem.ts │ ├── cacheSizeSolution.ts │ ├── setMaxSize.ts │ ├── usingWithCreateSelector.ts │ ├── usingWithCreateSelectorCreator.ts │ └── withUseMemo.tsx ├── netlify.toml ├── package.json ├── scripts └── writeGitVersion.mjs ├── src ├── autotrackMemoize │ ├── autotrackMemoize.ts │ ├── autotracking.ts │ ├── proxy.ts │ ├── tracking.ts │ └── utils.ts ├── createSelectorCreator.ts ├── createStructuredSelector.ts ├── devModeChecks │ ├── identityFunctionCheck.ts │ ├── inputStabilityCheck.ts │ └── setGlobalDevModeChecks.ts ├── index.ts ├── lruMemoize.ts ├── types.ts ├── utils.ts ├── versionedTypes │ ├── index.ts │ └── ts47-mergeParameters.ts └── weakMapMemoize.ts ├── test ├── autotrackMemoize.spec.ts ├── benchmarks │ ├── orderOfExecution.bench.ts │ ├── reselect.bench.ts │ ├── resultEqualityCheck.bench.ts │ └── weakMapMemoize.bench.ts ├── computationComparisons.spec.tsx ├── createSelector.withTypes.test.ts ├── createStructuredSelector.spec.ts ├── createStructuredSelector.withTypes.test.ts ├── customMatchers.d.ts ├── examples.test.ts ├── identityFunctionCheck.test.ts ├── inputStabilityCheck.spec.ts ├── lruMemoize.test.ts ├── perfComparisons.spec.ts ├── reselect.spec.ts ├── selectorUtils.spec.ts ├── setup.vitest.ts ├── testTypes.ts ├── testUtils.ts ├── tsconfig.json └── weakmapMemoize.spec.ts ├── tsconfig.json ├── tsup.config.ts ├── type-tests ├── argsMemoize.test-d.ts ├── createSelector.withTypes.test-d.ts ├── createSelectorCreator.test-d.ts ├── createStructuredSelector.test-d.ts ├── createStructuredSelector.withTypes.test-d.ts ├── deepNesting.test-d.ts └── tsconfig.json ├── typescript_test ├── argsMemoize.typetest.ts ├── test.ts ├── tsconfig.json └── typesTestUtils.ts ├── vitest.config.mts ├── website ├── .gitignore ├── README.md ├── babel.config.js ├── compileExamples.ts ├── docs │ ├── FAQ.mdx │ ├── api │ │ ├── createSelector.mdx │ │ ├── createSelectorCreator.mdx │ │ ├── createStructuredSelector.mdx │ │ ├── development-only-stability-checks.mdx │ │ ├── lruMemoize.mdx │ │ ├── unstable_autotrackMemoize.mdx │ │ └── weakMapMemoize.mdx │ ├── external-references.mdx │ ├── introduction │ │ ├── getting-started.mdx │ │ ├── how-does-reselect-work.mdx │ │ └── v5-summary.mdx │ ├── related-projects.mdx │ └── usage │ │ ├── best-practices.mdx │ │ ├── common-mistakes.mdx │ │ └── handling-empty-array-results.mdx ├── docusaurus.config.ts ├── insertCodeExamples.ts ├── monokaiTheme.js ├── package.json ├── sidebars.ts ├── src │ ├── components │ │ ├── ExternalLinks.tsx │ │ ├── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── InternalLinks.tsx │ │ └── PackageManagerTabs.tsx │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ └── index.tsx ├── static │ ├── .nojekyll │ └── img │ │ ├── diagrams │ │ ├── normal-memoization-function.drawio │ │ └── reselect-memoization.drawio │ │ ├── docusaurus-social-card.jpg │ │ ├── docusaurus.png │ │ ├── favicon.ico │ │ ├── logo.svg │ │ ├── normal-memoization-function.png │ │ ├── reselect-memoization.png │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ └── undraw_docusaurus_tree.svg ├── tsconfig.json └── yarn.lock └── yarn.lock /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["vanilla", "vanilla-ts"], 3 | "node": "20" 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | es 3 | dist 4 | node_modules 5 | vitest.config.ts 6 | tsup.config.ts -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module", 9 | "ecmaVersion": 2015 10 | }, 11 | "rules": { 12 | "array-bracket-spacing": [0], 13 | "comma-dangle": [2, "never"], 14 | "eol-last": 2, 15 | "no-multiple-empty-lines": 2, 16 | "object-curly-spacing": [2, "always"], 17 | "quotes": [ 18 | 2, 19 | "single", 20 | { "avoidEscape": true, "allowTemplateLiterals": true } 21 | ], 22 | "semi": [2, "never"], 23 | "strict": 0, 24 | "space-before-blocks": [2, "always"], 25 | "space-before-function-paren": [0] 26 | }, 27 | "overrides": [ 28 | { 29 | "parser": "@typescript-eslint/parser", 30 | "files": ["*.{c,m,}{t,j}s", "*.{t,j}sx"], 31 | "parserOptions": { 32 | "ecmaVersion": 2020, 33 | "sourceType": "module", 34 | "project": true 35 | }, 36 | "env": { "jest": true }, 37 | "extends": [ 38 | "eslint:recommended", 39 | "plugin:@typescript-eslint/eslint-recommended", 40 | "plugin:@typescript-eslint/recommended" 41 | ], 42 | "rules": { 43 | "@typescript-eslint/ban-ts-comment": "off", 44 | "@typescript-eslint/explicit-function-return-type": "off", 45 | "@typescript-eslint/explicit-module-boundary-types": "off", 46 | "@typescript-eslint/no-explicit-any": "off", 47 | "@typescript-eslint/no-unused-vars": "off", 48 | "@typescript-eslint/no-non-null-assertion": "off", 49 | "@typescript-eslint/no-shadow": ["off"], 50 | "@typescript-eslint/no-use-before-define": ["off"], 51 | "@typescript-eslint/ban-types": "off", 52 | "prefer-rest-params": "off", 53 | "prefer-spread": "off", 54 | "@typescript-eslint/consistent-type-imports": [ 55 | 2, 56 | { "fixStyle": "separate-type-imports" } 57 | ], 58 | "@typescript-eslint/consistent-type-exports": [2] 59 | } 60 | }, 61 | { 62 | "files": ["**/test/**/*.ts", "**/typescript_test/**/*.ts"], 63 | "rules": { 64 | "consistent-return": "off", 65 | "max-lines": "off", 66 | "@typescript-eslint/ban-ts-comment": "off", 67 | "@typescript-eslint/explicit-function-return-type": "off", 68 | "@typescript-eslint/no-empty-function": "off", 69 | "@typescript-eslint/no-explicit-any": "off", 70 | "@typescript-eslint/no-floating-promises": "off", 71 | "@typescript-eslint/no-non-null-assertion": "off", 72 | "@typescript-eslint/no-unused-vars": "off", 73 | "@typescript-eslint/no-shadow": "off" 74 | } 75 | }, 76 | { 77 | "parser": "@typescript-eslint/parser", 78 | "files": ["./docs/examples/**/*.{js,ts,jsx,tsx}"], 79 | "parserOptions": { 80 | "ecmaVersion": 2023, 81 | "sourceType": "module", 82 | "ecmaFeatures": { "jsx": true } 83 | }, 84 | "rules": { 85 | "no-unused-vars": [0] 86 | } 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test-types.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Build and Test on Node ${{ matrix.node }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node: ['22.x'] 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Node 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node }} 21 | cache: 'yarn' 22 | 23 | - name: Install dependencies 24 | run: yarn install 25 | 26 | # Read existing version, reuse that, add a Git short hash 27 | - name: Set build version to Git commit 28 | run: node scripts/writeGitVersion.mjs $(git rev-parse --short HEAD) 29 | 30 | - name: Check updated version 31 | run: jq .version package.json 32 | 33 | - name: Run linter 34 | run: yarn lint 35 | 36 | - name: Run tests 37 | run: yarn test 38 | 39 | - name: Pack 40 | run: yarn pack 41 | 42 | - uses: actions/upload-artifact@v4 43 | with: 44 | name: package 45 | path: ./package.tgz 46 | 47 | test-types: 48 | name: Test Types with TypeScript ${{ matrix.ts }} 49 | 50 | needs: [build] 51 | runs-on: ubuntu-latest 52 | strategy: 53 | fail-fast: false 54 | matrix: 55 | node: ['22.x'] 56 | ts: ['5.0', '5.1', '5.2', '5.3', '5.4', '5.5', '5.6', '5.7', '5.8'] 57 | 58 | steps: 59 | - name: Checkout repo 60 | uses: actions/checkout@v4 61 | 62 | - name: Use node ${{ matrix.node }} 63 | uses: actions/setup-node@v4 64 | with: 65 | node-version: ${{ matrix.node }} 66 | cache: 'yarn' 67 | 68 | - name: Install deps 69 | run: yarn install 70 | 71 | # Build with the actual TS version in the repo 72 | - name: Pack 73 | run: yarn build && yarn pack 74 | 75 | - name: Install build artifact 76 | run: yarn add ./package.tgz 77 | 78 | # Then install the specific version to test against 79 | - name: Install TypeScript ${{ matrix.ts }} 80 | run: yarn add --dev typescript@${{ matrix.ts }} 81 | 82 | - name: 'Remove source to ensure packaged types are used' 83 | run: rm -rf src 84 | 85 | # Remove config line that points "reselect" to the `src` folder, 86 | # so that the typetest will use the installed version instead 87 | - run: sed -i -e /@remap-prod-remove-line/d ./typescript_test/tsconfig.json vitest.config.mts 88 | 89 | - name: Test types 90 | run: | 91 | ./node_modules/.bin/tsc --version 92 | yarn test:typescript 93 | 94 | are-the-types-wrong: 95 | name: Check package config with are-the-types-wrong 96 | 97 | needs: [build] 98 | runs-on: ubuntu-latest 99 | strategy: 100 | fail-fast: false 101 | matrix: 102 | node: ['22.x'] 103 | steps: 104 | - name: Checkout repo 105 | uses: actions/checkout@v4 106 | 107 | - name: Use node ${{ matrix.node }} 108 | uses: actions/setup-node@v4 109 | with: 110 | node-version: ${{ matrix.node }} 111 | cache: 'yarn' 112 | 113 | - uses: actions/download-artifact@v4 114 | with: 115 | name: package 116 | path: . 117 | 118 | # Note: We currently expect "FalseCJS" failures for Node16 + `moduleResolution: "node16", 119 | - name: Run are-the-types-wrong 120 | run: npx @arethetypeswrong/cli@latest ./package.tgz --format table --ignore-rules false-cjs 121 | 122 | test-published-artifact: 123 | name: Test Published Artifact ${{ matrix.example }} 124 | 125 | needs: [build] 126 | runs-on: ubuntu-latest 127 | strategy: 128 | fail-fast: false 129 | matrix: 130 | node: ['22.x'] 131 | example: 132 | [ 133 | 'cra4', 134 | 'cra5', 135 | 'next', 136 | 'vite', 137 | 'node-standard', 138 | 'node-esm', 139 | 'react-native', 140 | 'expo' 141 | ] 142 | steps: 143 | - name: Checkout repo 144 | uses: actions/checkout@v4 145 | 146 | - name: Use node ${{ matrix.node }} 147 | uses: actions/setup-node@v4 148 | with: 149 | node-version: ${{ matrix.node }} 150 | cache: 'yarn' 151 | 152 | - name: Clone RTK repo 153 | run: git clone https://github.com/reduxjs/redux-toolkit.git ./redux-toolkit 154 | 155 | - name: Check folder contents 156 | run: ls -l . 157 | 158 | - name: Install example deps 159 | working-directory: ./redux-toolkit/examples/publish-ci/${{ matrix.example }} 160 | run: yarn install 161 | 162 | - name: Install Playwright browser if necessary 163 | working-directory: ./redux-toolkit/examples/publish-ci/${{ matrix.example }} 164 | continue-on-error: true 165 | run: yarn playwright install || true 166 | 167 | - uses: actions/download-artifact@v4 168 | with: 169 | name: package 170 | path: ./redux-toolkit/examples/publish-ci/${{ matrix.example }} 171 | 172 | - name: Check folder contents 173 | working-directory: ./redux-toolkit/examples/publish-ci/${{ matrix.example }} 174 | run: ls -l . 175 | 176 | - name: Install build artifact 177 | working-directory: ./redux-toolkit/examples/publish-ci/${{ matrix.example }} 178 | run: yarn add ./package.tgz 179 | 180 | - name: Show installed package versions 181 | working-directory: ./redux-toolkit/examples/publish-ci/${{ matrix.example }} 182 | run: yarn info reselect && yarn why reselect 183 | 184 | - name: Set up JDK 17 for React Native build 185 | if: matrix.example == 'react-native' 186 | uses: actions/setup-java@v4 187 | with: 188 | java-version: '17.x' 189 | distribution: 'temurin' 190 | 191 | - name: Check MSW version 192 | working-directory: ./redux-toolkit/examples/publish-ci/${{ matrix.example }} 193 | run: yarn why msw 194 | 195 | - name: Build example 196 | working-directory: ./redux-toolkit/examples/publish-ci/${{ matrix.example }} 197 | env: 198 | NODE_OPTIONS: --openssl-legacy-provider 199 | run: yarn build 200 | 201 | - name: Run test step 202 | working-directory: ./redux-toolkit/examples/publish-ci/${{ matrix.example }} 203 | run: yarn test 204 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | # keeping it purely manual for now as to not accidentally trigger a release 4 | #release: 5 | # types: [published] 6 | workflow_dispatch: 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | id-token: write 12 | contents: read 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: '22.x' 18 | registry-url: 'https://registry.npmjs.org' 19 | cache: 'yarn' 20 | - run: yarn install --frozen-lockfile 21 | - run: yarn test 22 | - run: npm publish --access public --provenance 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .nyc_output 4 | coverage 5 | dist 6 | es 7 | .vscode 8 | .idea 9 | typescript_test/should_compile/index.js 10 | typescript_test/should_not_compile/index.js 11 | typescript_test/common.js 12 | flow_test/should_fail/flow-typed/index.js.flow 13 | flow_test/should_pass/flow-typed/index.js.flow 14 | reselect-builds/ 15 | trace 16 | 17 | typesversions 18 | .cache 19 | .yarnrc 20 | .yarn/* 21 | !.yarn/patches 22 | !.yarn/releases 23 | !.yarn/plugins 24 | !.yarn/sdks 25 | !.yarn/versions 26 | .pnp.* 27 | *.tgz 28 | 29 | website/translated_docs 30 | website/build/ 31 | website/node_modules 32 | website/i18n/* 33 | website/.yarn/ 34 | 35 | docs/examples/**/*.js 36 | docs/examples/**/*.jsx 37 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "none", 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "after:bump": "yarn && git add -u" 4 | }, 5 | "git": { 6 | "tagName": "v${version}" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | enableTransparentWorkspaces: false 6 | 7 | nodeLinker: node-modules 8 | 9 | yarnPath: .yarn/releases/yarn-4.4.1.cjs 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Lee Bannard (https://github.com/ellbee) 2 | Martijn Faassen (https://github.com/faassen) 3 | Ian Ker-Seymer (https://github.com/ianks) 4 | Mike S (https://github.com/SpainTrain) 5 | Daniel Bugl (https://github.com/omnidan) 6 | Ryan (https://github.com/ryanatkn) 7 | Alex Guerra (https://github.com/HeyImAlex) 8 | speedskater (https://github.com/speedskater) 9 | Daniela Borges (https://github.com/sericaia) 10 | Brian Ng (https://github.com/existentialism) 11 | C. T. Lin (https://github.com/chentsulin) 12 | Jay (https://github.com/chungchiehlun) 13 | Christian Schuhmann (https://github.com/madebyherzblut) 14 | Daniel Barreto (https://github.com/volrath) 15 | Adam Royle (https://github.com/ifunk) 16 | Elliot Crosby-McCullough (https://github.com/elliotcm) 17 | frankwallis (https://github.com/frankwallis) 18 | Jason Huang (https://github.com/kaddopur) 19 | Josh Kelley (https://github.com/joshkel) 20 | Leon Aves (https://github.com/leonaves) 21 | Mark Dalgleish (https://github.com/markdalgleish) 22 | Max Goodman (https://github.com/chromakode) 23 | Michael Lancaster (https://github.com/weblancaster) 24 | Mihail Diordiev (https://github.com/zalmoxisus) 25 | PSpSynedra (https://github.com/PSpSynedra) 26 | Simen Bekkhus (https://github.com/SimenB) 27 | Wade Peterson (https://github.com/WadePeterson) 28 | 长天之云 (https://github.com/ambar) 29 | Courtland Allen (https://github.com/courthead) 30 | Henrik Joreteg (https://github.com/HenrikJoreteg) 31 | Kyle Davis (https://github.com/kyldvs) 32 | Salvador Hernandez (https://github.com/clickclickonsal) 33 | Nick Ball (https://github.com/npbee) 34 | mctep (https://github.com/mctep) 35 | Jacob Rask (https://github.com/jacobrask) 36 | Luqmaan Dawoodjee (https://github.com/luqmaan) 37 | Walter Breakell (https://github.com/wbreakell) 38 | Matthew Hetherington (https://github.com/matthetherington) 39 | Mike Wilcox (https://github.com/mjw56) 40 | David Edmondson (https://github.com/threehams) 41 | Andrey Zaytsev (https://github.com/zandroid) 42 | 1ven (https://github.com/1ven) 43 | Alexey Yurchenko (https://github.com/alexesdev) 44 | Douglas Russell (https://github.com/dpwrussell) 45 | Yonatan Kogan (https://github.com/yoni-tock) 46 | Peter Petrov (https://github.com/pesho) 47 | Walter Breakell (https://github.com/wbreakell) 48 | Whien (https://github.com/madeinfree) 49 | Sergei Egorov (https://github.com/bsideup) 50 | Jim Bolla (https://github.com/jimbolla) 51 | Carl Bernardo (https://github.com/carlbernrdo) 52 | Daniel Lytkin (https://github.com/aikoven) 53 | John Haley (https://github.com/johnhaley81) 54 | Alexandre (https://github.com/alex3165) 55 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | reselect.js.org 2 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | # CREDITS 2 | 3 | * Based on a proposal for Redux by [Robert Binna](https://github.com/speedskater) and Philip Spitzlinger. 4 | Lots of the basic structure of the code is thanks to this. 5 | 6 | * Refactored into reselect library by Martijn Faassen and Lee Bannard 7 | at the React Europe Hackathon 2015. Also added tests. 8 | 9 | * Contributors: Lee Bannard, Martijn Faassen, Robert Binna, Alex 10 | Guerra, ryanatkn, Adam Royle, Christian Schuhmann, Jason Huang, 11 | Daniel Barreto, Mihail Diordiev, Daniela Borges, Philip Spitzlinger, 12 | C. T. Lin, SpainTrain, Mark Dalgleish, Brian Ng, 长天之云, Michael Lancaster, 13 | Elliot Crosby-McCullough, Max Goodman, Simen Bekkhus, Wade Peterson, 14 | chungchiehlun, Dave Hendler, Leon Aves, Ian Ker-Seymer, Josh Kelley, 15 | Daniel Bugl, Courtland Allen, Henrik Joreteg, Kyle Davis, Nick Ball, 16 | Salvador Hernandez, mctep, Jacob Rask, Luqmaan Dawoodjee, Walter Breakell, 17 | Matthew Hetherington, Mike Wilcox, David Edmondson, Andrey Zaytsev, 1ven, 18 | Alexey Yurchenko, Douglas Russell, Yonatan Kogan, Peter Petrov, 19 | Whien, Sergei Egorov, Jim Bolla, Carl Bernardo, Daniel Lytkin, John Haley, 20 | alex3165, 21 | 22 | * Inspired by getters in Nuclear.js and subscriptions in re-frame. 23 | 24 | * Special thanks to [David Edmonson](https://github.com/threehams) for maintaining the Typescript type definitions. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018 Reselect Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "reach, diff, flags, files" 3 | behavior: default 4 | require_changes: false # if true: only post the comment if coverage changes 5 | require_base: no # [yes :: must have a base report to post] 6 | require_head: no # [yes :: must have a head report to post] 7 | -------------------------------------------------------------------------------- /docs/examples/FAQ/MyComponent.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import { useSelectTodo } from './createParametricSelectorHook' 3 | 4 | interface Props { 5 | id: number 6 | } 7 | 8 | const MyComponent: FC = ({ id }) => { 9 | const todo = useSelectTodo(id) 10 | return
{todo?.title}
11 | } 12 | -------------------------------------------------------------------------------- /docs/examples/FAQ/createCurriedSelector.ts: -------------------------------------------------------------------------------- 1 | import type { weakMapMemoize, SelectorArray, UnknownMemoizer } from 'reselect' 2 | import { createSelector } from 'reselect' 3 | import { currySelector } from './currySelector' 4 | 5 | export const createCurriedSelector = < 6 | InputSelectors extends SelectorArray, 7 | Result, 8 | OverrideMemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize, 9 | OverrideArgsMemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize 10 | >( 11 | ...args: Parameters< 12 | typeof createSelector< 13 | InputSelectors, 14 | Result, 15 | OverrideMemoizeFunction, 16 | OverrideArgsMemoizeFunction 17 | > 18 | > 19 | ) => { 20 | return currySelector(createSelector(...args)) 21 | } 22 | -------------------------------------------------------------------------------- /docs/examples/FAQ/createParametricSelectorHook.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | import { createSelector } from 'reselect' 3 | 4 | interface RootState { 5 | todos: { 6 | id: number 7 | completed: boolean 8 | title: string 9 | description: string 10 | }[] 11 | alerts: { id: number; read: boolean }[] 12 | } 13 | 14 | const state: RootState = { 15 | todos: [ 16 | { 17 | id: 0, 18 | completed: false, 19 | title: 'Figure out if plants are really plotting world domination.', 20 | description: 'They may be.' 21 | }, 22 | { 23 | id: 1, 24 | completed: true, 25 | title: 'Practice telekinesis for 15 minutes', 26 | description: 'Just do it' 27 | } 28 | ], 29 | alerts: [ 30 | { id: 0, read: false }, 31 | { id: 1, read: true } 32 | ] 33 | } 34 | 35 | const selectTodoById = createSelector( 36 | [(state: RootState) => state.todos, (state: RootState, id: number) => id], 37 | (todos, id) => todos.find(todo => todo.id === id) 38 | ) 39 | 40 | export const createParametricSelectorHook = < 41 | Result, 42 | Params extends readonly unknown[] 43 | >( 44 | selector: (state: RootState, ...params: Params) => Result 45 | ) => { 46 | return (...args: Params) => { 47 | return useSelector((state: RootState) => selector(state, ...args)) 48 | } 49 | } 50 | 51 | export const useSelectTodo = createParametricSelectorHook(selectTodoById) 52 | -------------------------------------------------------------------------------- /docs/examples/FAQ/currySelector.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import type { RootState } from './selectorRecomputing' 3 | 4 | export const currySelector = < 5 | State, 6 | Result, 7 | Params extends readonly any[], 8 | AdditionalFields 9 | >( 10 | selector: ((state: State, ...args: Params) => Result) & AdditionalFields 11 | ) => { 12 | const curriedSelector = (...args: Params) => { 13 | return (state: State) => { 14 | return selector(state, ...args) 15 | } 16 | } 17 | return Object.assign(curriedSelector, selector) 18 | } 19 | 20 | const selectTodoByIdCurried = currySelector( 21 | createSelector( 22 | [(state: RootState) => state.todos, (state: RootState, id: number) => id], 23 | (todos, id) => todos.find(todo => todo.id === id) 24 | ) 25 | ) 26 | -------------------------------------------------------------------------------- /docs/examples/FAQ/howToTest.test.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { expect, test } from 'vitest' 3 | 4 | interface RootState { 5 | todos: { id: number; completed: boolean }[] 6 | alerts: { id: number; read: boolean }[] 7 | } 8 | 9 | const state: RootState = { 10 | todos: [ 11 | { id: 0, completed: false }, 12 | { id: 1, completed: true } 13 | ], 14 | alerts: [ 15 | { id: 0, read: false }, 16 | { id: 1, read: true } 17 | ] 18 | } 19 | 20 | // With `Vitest` or `Jest` 21 | test('selector unit test', () => { 22 | const selectTodoIds = createSelector( 23 | [(state: RootState) => state.todos], 24 | todos => todos.map(({ id }) => id) 25 | ) 26 | const firstResult = selectTodoIds(state) 27 | const secondResult = selectTodoIds(state) 28 | // Reference equality should pass. 29 | expect(firstResult).toBe(secondResult) 30 | // Deep equality should also pass. 31 | expect(firstResult).toStrictEqual(secondResult) 32 | selectTodoIds(state) 33 | selectTodoIds(state) 34 | selectTodoIds(state) 35 | // The result function should not recalculate. 36 | expect(selectTodoIds.recomputations()).toBe(1) 37 | // input selectors should not recalculate. 38 | expect(selectTodoIds.dependencyRecomputations()).toBe(1) 39 | }) 40 | 41 | // With `Chai` 42 | test('selector unit test', () => { 43 | const selectTodoIds = createSelector( 44 | [(state: RootState) => state.todos], 45 | todos => todos.map(({ id }) => id) 46 | ) 47 | const firstResult = selectTodoIds(state) 48 | const secondResult = selectTodoIds(state) 49 | // Reference equality should pass. 50 | expect(firstResult).to.equal(secondResult) 51 | // Deep equality should also pass. 52 | expect(firstResult).to.deep.equal(secondResult) 53 | selectTodoIds(state) 54 | selectTodoIds(state) 55 | selectTodoIds(state) 56 | // The result function should not recalculate. 57 | expect(selectTodoIds.recomputations()).to.equal(1) 58 | // input selectors should not recalculate. 59 | expect(selectTodoIds.dependencyRecomputations()).to.equal(1) 60 | }) 61 | -------------------------------------------------------------------------------- /docs/examples/FAQ/identity.ts: -------------------------------------------------------------------------------- 1 | import { createSelectorCreator } from 'reselect' 2 | 3 | const identity = any>(func: Func) => func 4 | 5 | const createNonMemoizedSelector = createSelectorCreator({ 6 | memoize: identity, 7 | argsMemoize: identity 8 | }) 9 | -------------------------------------------------------------------------------- /docs/examples/FAQ/selectorRecomputing.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, lruMemoize } from 'reselect' 2 | 3 | export interface RootState { 4 | todos: { id: number; completed: boolean }[] 5 | alerts: { id: number; read: boolean; type: string }[] 6 | } 7 | 8 | const selectAlertsByType = createSelector( 9 | [ 10 | (state: RootState) => state.alerts, 11 | (state: RootState, type: string) => type 12 | ], 13 | (alerts, type) => alerts.filter(todo => todo.type === type), 14 | { 15 | argsMemoize: lruMemoize, 16 | argsMemoizeOptions: { 17 | // This will check the arguments passed to the output selector. 18 | equalityCheck: (a, b) => { 19 | if (a !== b) { 20 | console.log('Changed argument:', a, 'to', b) 21 | } 22 | return a === b 23 | } 24 | } 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /docs/examples/basicUsage.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | interface RootState { 4 | todos: { id: number; completed: boolean }[] 5 | alerts: { id: number; read: boolean }[] 6 | } 7 | 8 | const state: RootState = { 9 | todos: [ 10 | { id: 0, completed: false }, 11 | { id: 1, completed: true } 12 | ], 13 | alerts: [ 14 | { id: 0, read: false }, 15 | { id: 1, read: true } 16 | ] 17 | } 18 | 19 | const selectCompletedTodos = (state: RootState) => { 20 | console.log('selector ran') 21 | return state.todos.filter(todo => todo.completed === true) 22 | } 23 | 24 | selectCompletedTodos(state) // selector ran 25 | selectCompletedTodos(state) // selector ran 26 | selectCompletedTodos(state) // selector ran 27 | 28 | const memoizedSelectCompletedTodos = createSelector( 29 | [(state: RootState) => state.todos], 30 | todos => { 31 | console.log('memoized selector ran') 32 | return todos.filter(todo => todo.completed === true) 33 | } 34 | ) 35 | 36 | memoizedSelectCompletedTodos(state) // memoized selector ran 37 | memoizedSelectCompletedTodos(state) 38 | memoizedSelectCompletedTodos(state) 39 | 40 | console.log(selectCompletedTodos(state) === selectCompletedTodos(state)) //=> false 41 | 42 | console.log( 43 | memoizedSelectCompletedTodos(state) === memoizedSelectCompletedTodos(state) 44 | ) //=> true 45 | -------------------------------------------------------------------------------- /docs/examples/createSelector/annotateResultFunction.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | interface Todo { 4 | id: number 5 | completed: boolean 6 | } 7 | 8 | interface Alert { 9 | id: number 10 | read: boolean 11 | } 12 | 13 | export interface RootState { 14 | todos: Todo[] 15 | alerts: Alert[] 16 | } 17 | 18 | export const createAppSelector = createSelector.withTypes() 19 | 20 | const selectTodoIds = createAppSelector( 21 | // Type of `state` is set to `RootState`, no need to manually set the type 22 | state => state.todos, 23 | // ❌ Known limitation: Parameter types are not inferred in this scenario 24 | // so you will have to manually annotate them. 25 | // highlight-start 26 | (todos: Todo[]) => todos.map(({ id }) => id) 27 | // highlight-end 28 | ) 29 | -------------------------------------------------------------------------------- /docs/examples/createSelector/createAppSelector.ts: -------------------------------------------------------------------------------- 1 | import microMemoize from 'micro-memoize' 2 | import { shallowEqual } from 'react-redux' 3 | import { createSelectorCreator, lruMemoize } from 'reselect' 4 | 5 | export interface RootState { 6 | todos: { id: number; completed: boolean }[] 7 | alerts: { id: number; read: boolean }[] 8 | } 9 | 10 | export const createAppSelector = createSelectorCreator({ 11 | memoize: lruMemoize, 12 | argsMemoize: microMemoize, 13 | memoizeOptions: { 14 | maxSize: 10, 15 | equalityCheck: shallowEqual, 16 | resultEqualityCheck: shallowEqual 17 | }, 18 | argsMemoizeOptions: { 19 | isEqual: shallowEqual, 20 | maxSize: 10 21 | }, 22 | devModeChecks: { 23 | identityFunctionCheck: 'never', 24 | inputStabilityCheck: 'always' 25 | } 26 | }).withTypes() 27 | 28 | const selectReadAlerts = createAppSelector( 29 | [ 30 | // Type of `state` is set to `RootState`, no need to manually set the type 31 | // highlight-start 32 | state => state.alerts 33 | // highlight-end 34 | ], 35 | alerts => alerts.filter(({ read }) => read) 36 | ) 37 | -------------------------------------------------------------------------------- /docs/examples/createSelector/withTypes.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | export interface RootState { 4 | todos: { id: number; completed: boolean }[] 5 | alerts: { id: number; read: boolean }[] 6 | } 7 | 8 | export const createAppSelector = createSelector.withTypes() 9 | 10 | const selectTodoIds = createAppSelector( 11 | [ 12 | // Type of `state` is set to `RootState`, no need to manually set the type 13 | // highlight-start 14 | state => state.todos 15 | // highlight-end 16 | ], 17 | todos => todos.map(({ id }) => id) 18 | ) 19 | -------------------------------------------------------------------------------- /docs/examples/createStructuredSelector/MyComponent.tsx: -------------------------------------------------------------------------------- 1 | import type { RootState } from 'createStructuredSelector/modernUseCase' 2 | import { structuredSelector } from 'createStructuredSelector/modernUseCase' 3 | import type { FC } from 'react' 4 | import { useSelector } from 'react-redux' 5 | 6 | interface Props { 7 | id: number 8 | } 9 | 10 | const MyComponent: FC = ({ id }) => { 11 | const { todos, alerts, todoById } = useSelector((state: RootState) => 12 | structuredSelector(state, id) 13 | ) 14 | 15 | return ( 16 |
17 | Next to do is: 18 |

{todoById.title}

19 |

Description: {todoById.description}

20 |
    21 |

    All other to dos:

    22 | {todos.map(todo => ( 23 |
  • {todo.title}
  • 24 | ))} 25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /docs/examples/createStructuredSelector/modernUseCase.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, createStructuredSelector } from 'reselect' 2 | 3 | export interface RootState { 4 | todos: { 5 | id: number 6 | completed: boolean 7 | title: string 8 | description: string 9 | }[] 10 | alerts: { id: number; read: boolean }[] 11 | } 12 | 13 | // This: 14 | export const structuredSelector = createStructuredSelector( 15 | { 16 | todos: (state: RootState) => state.todos, 17 | alerts: (state: RootState) => state.alerts, 18 | todoById: (state: RootState, id: number) => state.todos[id] 19 | }, 20 | createSelector 21 | ) 22 | 23 | // Is essentially the same as this: 24 | export const selector = createSelector( 25 | [ 26 | (state: RootState) => state.todos, 27 | (state: RootState) => state.alerts, 28 | (state: RootState, id: number) => state.todos[id] 29 | ], 30 | (todos, alerts, todoById) => { 31 | return { 32 | todos, 33 | alerts, 34 | todoById 35 | } 36 | } 37 | ) 38 | -------------------------------------------------------------------------------- /docs/examples/createStructuredSelector/withTypes.ts: -------------------------------------------------------------------------------- 1 | import { createStructuredSelector } from 'reselect' 2 | 3 | export interface RootState { 4 | todos: { id: number; completed: boolean }[] 5 | alerts: { id: number; read: boolean }[] 6 | } 7 | 8 | export const createStructuredAppSelector = 9 | createStructuredSelector.withTypes() 10 | 11 | const structuredAppSelector = createStructuredAppSelector({ 12 | // Type of `state` is set to `RootState`, no need to manually set the type 13 | // highlight-start 14 | todos: state => state.todos, 15 | // highlight-end 16 | alerts: state => state.alerts, 17 | todoById: (state, id: number) => state.todos[id] 18 | }) 19 | -------------------------------------------------------------------------------- /docs/examples/development-only-stability-checks/identityFunctionCheck.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | interface RootState { 4 | todos: { id: number; completed: boolean }[] 5 | alerts: { id: number; read: boolean }[] 6 | } 7 | 8 | // Create a selector that checks to see if the result function is an identity function. 9 | const selectTodos = createSelector( 10 | // ✔️ GOOD: Contains extraction logic. 11 | [(state: RootState) => state.todos], 12 | // ❌ BAD: Does not contain transformation logic. 13 | todos => todos, 14 | // Will override the global setting. 15 | { devModeChecks: { identityFunctionCheck: 'always' } } 16 | ) 17 | -------------------------------------------------------------------------------- /docs/examples/development-only-stability-checks/inputStabilityCheck.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | interface RootState { 4 | todos: { id: number; completed: boolean }[] 5 | alerts: { id: number; read: boolean }[] 6 | } 7 | 8 | // Create a selector that double-checks the results of input selectors every time it runs. 9 | const selectCompletedTodosLength = createSelector( 10 | [ 11 | // ❌ Incorrect Use Case: This input selector will not be 12 | // memoized properly since it always returns a new reference. 13 | (state: RootState) => 14 | state.todos.filter(({ completed }) => completed === true) 15 | ], 16 | completedTodos => completedTodos.length, 17 | // Will override the global setting. 18 | { devModeChecks: { inputStabilityCheck: 'always' } } 19 | ) 20 | -------------------------------------------------------------------------------- /docs/examples/handling-empty-array-results/fallbackToEmptyArray.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import type { RootState } from './firstPattern' 3 | 4 | const EMPTY_ARRAY: [] = [] 5 | 6 | export const fallbackToEmptyArray = (array: T[]) => { 7 | return array.length === 0 ? EMPTY_ARRAY : array 8 | } 9 | 10 | const selectCompletedTodos = createSelector( 11 | [(state: RootState) => state.todos], 12 | todos => { 13 | return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) 14 | } 15 | ) 16 | -------------------------------------------------------------------------------- /docs/examples/handling-empty-array-results/firstPattern.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | export interface RootState { 4 | todos: { 5 | id: number 6 | title: string 7 | description: string 8 | completed: boolean 9 | }[] 10 | } 11 | 12 | const EMPTY_ARRAY: [] = [] 13 | 14 | const selectCompletedTodos = createSelector( 15 | [(state: RootState) => state.todos], 16 | todos => { 17 | const completedTodos = todos.filter(todo => todo.completed === true) 18 | return completedTodos.length === 0 ? EMPTY_ARRAY : completedTodos 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /docs/examples/lruMemoize/referenceEqualityCheck.ts: -------------------------------------------------------------------------------- 1 | const referenceEqualityCheck = (previousValue: any, currentValue: any) => { 2 | return previousValue === currentValue 3 | } 4 | -------------------------------------------------------------------------------- /docs/examples/lruMemoize/usingWithCreateSelector.ts: -------------------------------------------------------------------------------- 1 | import { shallowEqual } from 'react-redux' 2 | import { createSelector, lruMemoize } from 'reselect' 3 | 4 | export interface RootState { 5 | todos: { 6 | id: number 7 | completed: boolean 8 | title: string 9 | description: string 10 | }[] 11 | alerts: { id: number; read: boolean }[] 12 | } 13 | 14 | const selectTodoIds = createSelector( 15 | [(state: RootState) => state.todos], 16 | todos => todos.map(todo => todo.id), 17 | { 18 | memoize: lruMemoize, 19 | memoizeOptions: { 20 | equalityCheck: shallowEqual, 21 | resultEqualityCheck: shallowEqual, 22 | maxSize: 10 23 | }, 24 | argsMemoize: lruMemoize, 25 | argsMemoizeOptions: { 26 | equalityCheck: shallowEqual, 27 | resultEqualityCheck: shallowEqual, 28 | maxSize: 10 29 | } 30 | } 31 | ) 32 | -------------------------------------------------------------------------------- /docs/examples/lruMemoize/usingWithCreateSelectorCreator.ts: -------------------------------------------------------------------------------- 1 | import { shallowEqual } from 'react-redux' 2 | import { createSelectorCreator, lruMemoize } from 'reselect' 3 | 4 | export interface RootState { 5 | todos: { 6 | id: number 7 | completed: boolean 8 | title: string 9 | description: string 10 | }[] 11 | alerts: { id: number; read: boolean }[] 12 | } 13 | 14 | const createSelectorShallowEqual = createSelectorCreator({ 15 | memoize: lruMemoize, 16 | memoizeOptions: { 17 | equalityCheck: shallowEqual, 18 | resultEqualityCheck: shallowEqual, 19 | maxSize: 10 20 | }, 21 | argsMemoize: lruMemoize, 22 | argsMemoizeOptions: { 23 | equalityCheck: shallowEqual, 24 | resultEqualityCheck: shallowEqual, 25 | maxSize: 10 26 | } 27 | }) 28 | 29 | const selectTodoIds = createSelectorShallowEqual( 30 | [(state: RootState) => state.todos], 31 | todos => todos.map(todo => todo.id) 32 | ) 33 | -------------------------------------------------------------------------------- /docs/examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "noUnusedLocals": false, 5 | "noUnusedParameters": false, 6 | "allowUnusedLabels": true, 7 | "isolatedModules": true, 8 | "removeComments": false, 9 | "checkJs": true, 10 | "alwaysStrict": false, 11 | "baseUrl": ".", 12 | "outDir": "dist", 13 | "strict": true, 14 | "target": "ESNext", 15 | "module": "ESNext", 16 | "moduleResolution": "Node", 17 | "esModuleInterop": true, 18 | "skipLibCheck": true, 19 | "allowJs": true, 20 | "jsx": "preserve", 21 | "noErrorTruncation": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "experimentalDecorators": true, 24 | "paths": { 25 | "reselect": ["../../src/index"], // @remap-prod-remove-line 26 | "@internal/*": ["../../src/*"] 27 | } 28 | }, 29 | "include": ["**/*.ts", "**/*.tsx"] 30 | } 31 | -------------------------------------------------------------------------------- /docs/examples/unstable_autotrackMemoize/usingWithCreateSelector.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, unstable_autotrackMemoize } from 'reselect' 2 | 3 | export interface RootState { 4 | todos: { id: number; completed: boolean }[] 5 | alerts: { id: number; read: boolean }[] 6 | } 7 | 8 | const selectTodoIds = createSelector( 9 | [(state: RootState) => state.todos], 10 | todos => todos.map(todo => todo.id), 11 | { memoize: unstable_autotrackMemoize } 12 | ) 13 | -------------------------------------------------------------------------------- /docs/examples/unstable_autotrackMemoize/usingWithCreateSelectorCreator.ts: -------------------------------------------------------------------------------- 1 | import { createSelectorCreator, unstable_autotrackMemoize } from 'reselect' 2 | import type { RootState } from './usingWithCreateSelector' 3 | 4 | const createSelectorAutotrack = createSelectorCreator({ 5 | memoize: unstable_autotrackMemoize 6 | }) 7 | 8 | const selectTodoIds = createSelectorAutotrack( 9 | [(state: RootState) => state.todos], 10 | todos => todos.map(todo => todo.id) 11 | ) 12 | -------------------------------------------------------------------------------- /docs/examples/weakMapMemoize/cacheSizeProblem.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | export interface RootState { 4 | items: { id: number; category: string; name: string }[] 5 | } 6 | 7 | const state: RootState = { 8 | items: [ 9 | { id: 1, category: 'Electronics', name: 'Wireless Headphones' }, 10 | { id: 2, category: 'Books', name: 'The Great Gatsby' }, 11 | { id: 3, category: 'Home Appliances', name: 'Blender' }, 12 | { id: 4, category: 'Stationery', name: 'Sticky Notes' } 13 | ] 14 | } 15 | 16 | const selectItemsByCategory = createSelector( 17 | [ 18 | (state: RootState) => state.items, 19 | (state: RootState, category: string) => category 20 | ], 21 | (items, category) => items.filter(item => item.category === category) 22 | ) 23 | 24 | selectItemsByCategory(state, 'Electronics') // Selector runs 25 | selectItemsByCategory(state, 'Electronics') 26 | selectItemsByCategory(state, 'Stationery') // Selector runs 27 | selectItemsByCategory(state, 'Electronics') // Selector runs again! 28 | -------------------------------------------------------------------------------- /docs/examples/weakMapMemoize/cacheSizeSolution.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, weakMapMemoize } from 'reselect' 2 | import type { RootState } from './cacheSizeProblem' 3 | 4 | const state: RootState = { 5 | items: [ 6 | { id: 1, category: 'Electronics', name: 'Wireless Headphones' }, 7 | { id: 2, category: 'Books', name: 'The Great Gatsby' }, 8 | { id: 3, category: 'Home Appliances', name: 'Blender' }, 9 | { id: 4, category: 'Stationery', name: 'Sticky Notes' } 10 | ] 11 | } 12 | 13 | const selectItemsByCategory = createSelector( 14 | [ 15 | (state: RootState) => state.items, 16 | (state: RootState, category: string) => category 17 | ], 18 | (items, category) => items.filter(item => item.category === category), 19 | { 20 | memoize: weakMapMemoize, 21 | argsMemoize: weakMapMemoize 22 | } 23 | ) 24 | 25 | selectItemsByCategory(state, 'Electronics') // Selector runs 26 | selectItemsByCategory(state, 'Electronics') // Cached 27 | selectItemsByCategory(state, 'Stationery') // Selector runs 28 | selectItemsByCategory(state, 'Electronics') // Still cached! 29 | -------------------------------------------------------------------------------- /docs/examples/weakMapMemoize/setMaxSize.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, lruMemoize } from 'reselect' 2 | import type { RootState } from './cacheSizeProblem' 3 | 4 | const selectItemsByCategory = createSelector( 5 | [ 6 | (state: RootState) => state.items, 7 | (state: RootState, category: string) => category 8 | ], 9 | (items, category) => items.filter(item => item.category === category), 10 | { 11 | memoize: lruMemoize, 12 | memoizeOptions: { 13 | maxSize: 10 14 | } 15 | } 16 | ) 17 | -------------------------------------------------------------------------------- /docs/examples/weakMapMemoize/usingWithCreateSelector.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, weakMapMemoize } from 'reselect' 2 | import type { RootState } from './cacheSizeProblem' 3 | 4 | const state: RootState = { 5 | items: [ 6 | { id: 1, category: 'Electronics', name: 'Wireless Headphones' }, 7 | { id: 2, category: 'Books', name: 'The Great Gatsby' }, 8 | { id: 3, category: 'Home Appliances', name: 'Blender' }, 9 | { id: 4, category: 'Stationery', name: 'Sticky Notes' } 10 | ] 11 | } 12 | 13 | const selectItemsByCategory = createSelector( 14 | [ 15 | (state: RootState) => state.items, 16 | (state: RootState, category: string) => category 17 | ], 18 | (items, category) => items.filter(item => item.category === category), 19 | { 20 | memoize: weakMapMemoize, 21 | argsMemoize: weakMapMemoize 22 | } 23 | ) 24 | 25 | selectItemsByCategory(state, 'Electronics') // Selector runs 26 | selectItemsByCategory(state, 'Electronics') 27 | selectItemsByCategory(state, 'Stationery') // Selector runs 28 | selectItemsByCategory(state, 'Electronics') 29 | -------------------------------------------------------------------------------- /docs/examples/weakMapMemoize/usingWithCreateSelectorCreator.ts: -------------------------------------------------------------------------------- 1 | import { createSelectorCreator, weakMapMemoize } from 'reselect' 2 | import type { RootState } from './cacheSizeProblem' 3 | 4 | const state: RootState = { 5 | items: [ 6 | { id: 1, category: 'Electronics', name: 'Wireless Headphones' }, 7 | { id: 2, category: 'Books', name: 'The Great Gatsby' }, 8 | { id: 3, category: 'Home Appliances', name: 'Blender' }, 9 | { id: 4, category: 'Stationery', name: 'Sticky Notes' } 10 | ] 11 | } 12 | 13 | const createSelectorWeakMap = createSelectorCreator({ 14 | memoize: weakMapMemoize, 15 | argsMemoize: weakMapMemoize 16 | }) 17 | 18 | const selectItemsByCategory = createSelectorWeakMap( 19 | [ 20 | (state: RootState) => state.items, 21 | (state: RootState, category: string) => category 22 | ], 23 | (items, category) => items.filter(item => item.category === category) 24 | ) 25 | 26 | selectItemsByCategory(state, 'Electronics') // Selector runs 27 | selectItemsByCategory(state, 'Electronics') 28 | selectItemsByCategory(state, 'Stationery') // Selector runs 29 | selectItemsByCategory(state, 'Electronics') 30 | -------------------------------------------------------------------------------- /docs/examples/weakMapMemoize/withUseMemo.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import { useMemo } from 'react' 3 | import { useSelector } from 'react-redux' 4 | import { createSelector } from 'reselect' 5 | import type { RootState } from './cacheSizeProblem' 6 | 7 | const makeSelectItemsByCategory = (category: string) => 8 | createSelector([(state: RootState) => state.items], items => 9 | items.filter(item => item.category === category) 10 | ) 11 | 12 | interface Props { 13 | category: string 14 | } 15 | 16 | const MyComponent: FC = ({ category }) => { 17 | const selectItemsByCategory = useMemo( 18 | () => makeSelectItemsByCategory(category), 19 | [category] 20 | ) 21 | 22 | const itemsByCategory = useSelector(selectItemsByCategory) 23 | 24 | return ( 25 |
26 | {itemsByCategory.map(item => ( 27 |
{item.name}
28 | ))} 29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "website" 3 | publish = "build" 4 | command = "yarn build" 5 | ignore = "git diff --quiet HEAD^ HEAD -- ./docs/ ." 6 | 7 | [build.environment] 8 | NODE_VERSION = "20" 9 | NODE_OPTIONS = "--max_old_space_size=4096" 10 | NETLIFY_USE_YARN = "true" 11 | YARN_VERSION = "1.22.10" 12 | 13 | [[plugins]] 14 | package = "netlify-plugin-cache" 15 | [plugins.inputs] 16 | paths = [ 17 | "node_modules/.cache", 18 | "website/node_modules/.cache", 19 | ".yarn/.cache" 20 | ] 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reselect", 3 | "version": "5.1.1", 4 | "description": "Selectors for Redux.", 5 | "main": "./dist/cjs/index.js", 6 | "module": "./dist/reselect.legacy-esm.js", 7 | "types": "./dist/reselect.d.ts", 8 | "exports": { 9 | "./package.json": "./package.json", 10 | ".": { 11 | "types": "./dist/reselect.d.ts", 12 | "import": "./dist/reselect.mjs", 13 | "default": "./dist/cjs/index.js" 14 | } 15 | }, 16 | "files": [ 17 | "src", 18 | "dist" 19 | ], 20 | "sideEffects": false, 21 | "bugs": { 22 | "url": "https://github.com/reduxjs/reselect/issues" 23 | }, 24 | "scripts": { 25 | "build": "yarn clean && tsup", 26 | "clean": "rimraf dist", 27 | "format": "prettier --write \"{src,test}/**/*.{js,ts}\" \"docs/**/*.md\"", 28 | "lint": "eslint src test", 29 | "prepack": "yarn build", 30 | "bench": "vitest --run bench --mode production", 31 | "test": "node --expose-gc ./node_modules/vitest/dist/cli-wrapper.js --run && vitest --run --typecheck.only", 32 | "test:watch": "node --expose-gc ./node_modules/vitest/dist/cli-wrapper.js --watch", 33 | "test:cov": "vitest run --coverage", 34 | "type-check": "vitest --run --typecheck.only", 35 | "type-check:trace": "vitest --run --typecheck.only && tsc --noEmit -p typescript_test/tsconfig.json --generateTrace trace && npx @typescript/analyze-trace trace && rimraf trace", 36 | "test:typescript": "tsc --noEmit -p typescript_test/tsconfig.json", 37 | "docs:start": "yarn --cwd website start", 38 | "docs:build": "yarn --cwd website build", 39 | "docs:clear": "yarn --cwd website clear", 40 | "docs:serve": "yarn --cwd website serve" 41 | }, 42 | "keywords": [ 43 | "react", 44 | "redux" 45 | ], 46 | "authors": [ 47 | "Lee Bannard", 48 | "Robert Binna", 49 | "Martijn Faassen", 50 | "Philip Spitzlinger" 51 | ], 52 | "repository": { 53 | "type": "git", 54 | "url": "https://github.com/reduxjs/reselect.git" 55 | }, 56 | "license": "MIT", 57 | "devDependencies": { 58 | "@reduxjs/toolkit": "^2.0.1", 59 | "@testing-library/react": "^14.1.2", 60 | "@types/lodash": "^4.14.175", 61 | "@types/react": "^18.2.38", 62 | "@types/react-dom": "^18.2.17", 63 | "@types/shelljs": "^0.8.11", 64 | "@typescript-eslint/eslint-plugin": "^6", 65 | "@typescript-eslint/eslint-plugin-tslint": "^6", 66 | "@typescript-eslint/parser": "^6", 67 | "@typescript/analyze-trace": "^0.10.1", 68 | "eslint": "^8.0.1", 69 | "eslint-plugin-react": "^7.26.1", 70 | "eslint-plugin-typescript": "0.14.0", 71 | "jsdom": "^23.0.0", 72 | "lodash": "^4.17.21", 73 | "lodash.memoize": "^4.1.2", 74 | "memoize-one": "^6.0.0", 75 | "micro-memoize": "^4.0.9", 76 | "netlify-plugin-cache": "^1.0.3", 77 | "prettier": "^2.7.1", 78 | "react": "^18.2.0", 79 | "react-dom": "^18.2.0", 80 | "react-redux": "^9.0.4", 81 | "rimraf": "^3.0.2", 82 | "shelljs": "^0.8.5", 83 | "tsup": "^8.2.4", 84 | "typescript": "^5.8.2", 85 | "vitest": "^1.6.0" 86 | }, 87 | "resolutions": { 88 | "esbuild": "0.23.0" 89 | }, 90 | "packageManager": "yarn@4.4.1" 91 | } 92 | -------------------------------------------------------------------------------- /scripts/writeGitVersion.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = path.dirname(__filename) 7 | 8 | const gitRev = process.argv[2] 9 | 10 | const packagePath = path.join(__dirname, '../package.json') 11 | const pkg = JSON.parse(fs.readFileSync(packagePath)) 12 | 13 | pkg.version = `${pkg.version}-${gitRev}` 14 | fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2)) 15 | -------------------------------------------------------------------------------- /src/autotrackMemoize/autotrackMemoize.ts: -------------------------------------------------------------------------------- 1 | import { createNode, updateNode } from './proxy' 2 | import type { Node } from './tracking' 3 | 4 | import { createCacheKeyComparator, referenceEqualityCheck } from '../lruMemoize' 5 | import type { AnyFunction, DefaultMemoizeFields, Simplify } from '../types' 6 | import { createCache } from './autotracking' 7 | 8 | /** 9 | * Uses an "auto-tracking" approach inspired by the work of the Ember Glimmer team. 10 | * It uses a Proxy to wrap arguments and track accesses to nested fields 11 | * in your selector on first read. Later, when the selector is called with 12 | * new arguments, it identifies which accessed fields have changed and 13 | * only recalculates the result if one or more of those accessed fields have changed. 14 | * This allows it to be more precise than the shallow equality checks in `lruMemoize`. 15 | * 16 | * __Design Tradeoffs for `autotrackMemoize`:__ 17 | * - Pros: 18 | * - It is likely to avoid excess calculations and recalculate fewer times than `lruMemoize` will, 19 | * which may also result in fewer component re-renders. 20 | * - Cons: 21 | * - It only has a cache size of 1. 22 | * - It is slower than `lruMemoize`, because it has to do more work. (How much slower is dependent on the number of accessed fields in a selector, number of calls, frequency of input changes, etc) 23 | * - It can have some unexpected behavior. Because it tracks nested field accesses, 24 | * cases where you don't access a field will not recalculate properly. 25 | * For example, a badly-written selector like: 26 | * ```ts 27 | * createSelector([state => state.todos], todos => todos) 28 | * ``` 29 | * that just immediately returns the extracted value will never update, because it doesn't see any field accesses to check. 30 | * 31 | * __Use Cases for `autotrackMemoize`:__ 32 | * - It is likely best used for cases where you need to access specific nested fields 33 | * in data, and avoid recalculating if other fields in the same data objects are immutably updated. 34 | * 35 | * @param func - The function to be memoized. 36 | * @returns A memoized function with a `.clearCache()` method attached. 37 | * 38 | * @example 39 | * Using `createSelector` 40 | * ```ts 41 | * import { unstable_autotrackMemoize as autotrackMemoize, createSelector } from 'reselect' 42 | * 43 | * const selectTodoIds = createSelector( 44 | * [(state: RootState) => state.todos], 45 | * (todos) => todos.map(todo => todo.id), 46 | * { memoize: autotrackMemoize } 47 | * ) 48 | * ``` 49 | * 50 | * @example 51 | * Using `createSelectorCreator` 52 | * ```ts 53 | * import { unstable_autotrackMemoize as autotrackMemoize, createSelectorCreator } from 'reselect' 54 | * 55 | * const createSelectorAutotrack = createSelectorCreator({ memoize: autotrackMemoize }) 56 | * 57 | * const selectTodoIds = createSelectorAutotrack( 58 | * [(state: RootState) => state.todos], 59 | * (todos) => todos.map(todo => todo.id) 60 | * ) 61 | * ``` 62 | * 63 | * @template Func - The type of the function that is memoized. 64 | * 65 | * @see {@link https://reselect.js.org/api/unstable_autotrackMemoize autotrackMemoize} 66 | * 67 | * @since 5.0.0 68 | * @public 69 | * @experimental 70 | */ 71 | export function autotrackMemoize(func: Func) { 72 | // we reference arguments instead of spreading them for performance reasons 73 | 74 | const node: Node> = createNode( 75 | [] as unknown as Record 76 | ) 77 | 78 | let lastArgs: IArguments | null = null 79 | 80 | const shallowEqual = createCacheKeyComparator(referenceEqualityCheck) 81 | 82 | const cache = createCache(() => { 83 | const res = func.apply(null, node.proxy as unknown as any[]) 84 | return res 85 | }) 86 | 87 | function memoized() { 88 | if (!shallowEqual(lastArgs, arguments)) { 89 | updateNode(node, arguments as unknown as Record) 90 | lastArgs = arguments 91 | } 92 | return cache.value 93 | } 94 | 95 | memoized.clearCache = () => { 96 | return cache.clear() 97 | } 98 | 99 | return memoized as Func & Simplify 100 | } 101 | -------------------------------------------------------------------------------- /src/autotrackMemoize/autotracking.ts: -------------------------------------------------------------------------------- 1 | // Original autotracking implementation source: 2 | // - https://gist.github.com/pzuraq/79bf862e0f8cd9521b79c4b6eccdc4f9 3 | // Additional references: 4 | // - https://www.pzuraq.com/blog/how-autotracking-works 5 | // - https://v5.chriskrycho.com/journal/autotracking-elegant-dx-via-cutting-edge-cs/ 6 | import type { EqualityFn } from '../types' 7 | import { assertIsFunction } from '../utils' 8 | 9 | // The global revision clock. Every time state changes, the clock increments. 10 | export let $REVISION = 0 11 | 12 | // The current dependency tracker. Whenever we compute a cache, we create a Set 13 | // to track any dependencies that are used while computing. If no cache is 14 | // computing, then the tracker is null. 15 | let CURRENT_TRACKER: Set | TrackingCache> | null = null 16 | 17 | // Storage represents a root value in the system - the actual state of our app. 18 | export class Cell { 19 | revision = $REVISION 20 | 21 | _value: T 22 | _lastValue: T 23 | _isEqual: EqualityFn = tripleEq 24 | 25 | constructor(initialValue: T, isEqual: EqualityFn = tripleEq) { 26 | this._value = this._lastValue = initialValue 27 | this._isEqual = isEqual 28 | } 29 | 30 | // Whenever a storage value is read, it'll add itself to the current tracker if 31 | // one exists, entangling its state with that cache. 32 | get value() { 33 | CURRENT_TRACKER?.add(this) 34 | 35 | return this._value 36 | } 37 | 38 | // Whenever a storage value is updated, we bump the global revision clock, 39 | // assign the revision for this storage to the new value, _and_ we schedule a 40 | // rerender. This is important, and it's what makes autotracking _pull_ 41 | // based. We don't actively tell the caches which depend on the storage that 42 | // anything has happened. Instead, we recompute the caches when needed. 43 | set value(newValue) { 44 | if (this.value === newValue) return 45 | 46 | this._value = newValue 47 | this.revision = ++$REVISION 48 | } 49 | } 50 | 51 | function tripleEq(a: unknown, b: unknown) { 52 | return a === b 53 | } 54 | 55 | // Caches represent derived state in the system. They are ultimately functions 56 | // that are memoized based on what state they use to produce their output, 57 | // meaning they will only rerun IFF a storage value that could affect the output 58 | // has changed. Otherwise, they'll return the cached value. 59 | export class TrackingCache { 60 | _cachedValue: any 61 | _cachedRevision = -1 62 | _deps: any[] = [] 63 | hits = 0 64 | 65 | fn: () => any 66 | 67 | constructor(fn: () => any) { 68 | this.fn = fn 69 | } 70 | 71 | clear() { 72 | this._cachedValue = undefined 73 | this._cachedRevision = -1 74 | this._deps = [] 75 | this.hits = 0 76 | } 77 | 78 | get value() { 79 | // When getting the value for a Cache, first we check all the dependencies of 80 | // the cache to see what their current revision is. If the current revision is 81 | // greater than the cached revision, then something has changed. 82 | if (this.revision > this._cachedRevision) { 83 | const { fn } = this 84 | 85 | // We create a new dependency tracker for this cache. As the cache runs 86 | // its function, any Storage or Cache instances which are used while 87 | // computing will be added to this tracker. In the end, it will be the 88 | // full list of dependencies that this Cache depends on. 89 | const currentTracker = new Set>() 90 | const prevTracker = CURRENT_TRACKER 91 | 92 | CURRENT_TRACKER = currentTracker 93 | 94 | // try { 95 | this._cachedValue = fn() 96 | // } finally { 97 | CURRENT_TRACKER = prevTracker 98 | this.hits++ 99 | this._deps = Array.from(currentTracker) 100 | 101 | // Set the cached revision. This is the current clock count of all the 102 | // dependencies. If any dependency changes, this number will be less 103 | // than the new revision. 104 | this._cachedRevision = this.revision 105 | // } 106 | } 107 | 108 | // If there is a current tracker, it means another Cache is computing and 109 | // using this one, so we add this one to the tracker. 110 | CURRENT_TRACKER?.add(this) 111 | 112 | // Always return the cached value. 113 | return this._cachedValue 114 | } 115 | 116 | get revision() { 117 | // The current revision is the max of all the dependencies' revisions. 118 | return Math.max(...this._deps.map(d => d.revision), 0) 119 | } 120 | } 121 | 122 | export function getValue(cell: Cell): T { 123 | if (!(cell instanceof Cell)) { 124 | console.warn('Not a valid cell! ', cell) 125 | } 126 | 127 | return cell.value 128 | } 129 | 130 | type CellValue> = T extends Cell ? U : never 131 | 132 | export function setValue>( 133 | storage: T, 134 | value: CellValue 135 | ): void { 136 | if (!(storage instanceof Cell)) { 137 | throw new TypeError( 138 | 'setValue must be passed a tracked store created with `createStorage`.' 139 | ) 140 | } 141 | 142 | storage.value = storage._lastValue = value 143 | } 144 | 145 | export function createCell( 146 | initialValue: T, 147 | isEqual: EqualityFn = tripleEq 148 | ): Cell { 149 | return new Cell(initialValue, isEqual) 150 | } 151 | 152 | export function createCache(fn: () => T): TrackingCache { 153 | assertIsFunction( 154 | fn, 155 | 'the first parameter to `createCache` must be a function' 156 | ) 157 | 158 | return new TrackingCache(fn) 159 | } 160 | -------------------------------------------------------------------------------- /src/autotrackMemoize/proxy.ts: -------------------------------------------------------------------------------- 1 | // Original source: 2 | // - https://github.com/simonihmig/tracked-redux/blob/master/packages/tracked-redux/src/-private/proxy.ts 3 | 4 | import type { Node, Tag } from './tracking' 5 | import { 6 | consumeCollection, 7 | consumeTag, 8 | createTag, 9 | dirtyCollection, 10 | dirtyTag 11 | } from './tracking' 12 | 13 | export const REDUX_PROXY_LABEL = /* @__PURE__ */ Symbol() 14 | 15 | let nextId = 0 16 | 17 | const proto = /* @__PURE__ */ Object.getPrototypeOf({}) 18 | 19 | class ObjectTreeNode> implements Node { 20 | proxy: T = new Proxy(this, objectProxyHandler) as unknown as T 21 | tag = createTag() 22 | tags = {} as Record 23 | children = {} as Record 24 | collectionTag = null 25 | id = nextId++ 26 | 27 | constructor(public value: T) { 28 | this.value = value 29 | this.tag.value = value 30 | } 31 | } 32 | 33 | const objectProxyHandler = { 34 | get(node: Node, key: string | symbol): unknown { 35 | function calculateResult() { 36 | const { value } = node 37 | 38 | const childValue = Reflect.get(value, key) 39 | 40 | if (typeof key === 'symbol') { 41 | return childValue 42 | } 43 | 44 | if (key in proto) { 45 | return childValue 46 | } 47 | 48 | if (typeof childValue === 'object' && childValue !== null) { 49 | let childNode = node.children[key] 50 | 51 | if (childNode === undefined) { 52 | childNode = node.children[key] = createNode(childValue) 53 | } 54 | 55 | if (childNode.tag) { 56 | consumeTag(childNode.tag) 57 | } 58 | 59 | return childNode.proxy 60 | } else { 61 | let tag = node.tags[key] 62 | 63 | if (tag === undefined) { 64 | tag = node.tags[key] = createTag() 65 | tag.value = childValue 66 | } 67 | 68 | consumeTag(tag) 69 | 70 | return childValue 71 | } 72 | } 73 | const res = calculateResult() 74 | return res 75 | }, 76 | 77 | ownKeys(node: Node): ArrayLike { 78 | consumeCollection(node) 79 | return Reflect.ownKeys(node.value) 80 | }, 81 | 82 | getOwnPropertyDescriptor( 83 | node: Node, 84 | prop: string | symbol 85 | ): PropertyDescriptor | undefined { 86 | return Reflect.getOwnPropertyDescriptor(node.value, prop) 87 | }, 88 | 89 | has(node: Node, prop: string | symbol): boolean { 90 | return Reflect.has(node.value, prop) 91 | } 92 | } 93 | 94 | class ArrayTreeNode> implements Node { 95 | proxy: T = new Proxy([this], arrayProxyHandler) as unknown as T 96 | tag = createTag() 97 | tags = {} 98 | children = {} 99 | collectionTag = null 100 | id = nextId++ 101 | 102 | constructor(public value: T) { 103 | this.value = value 104 | this.tag.value = value 105 | } 106 | } 107 | 108 | const arrayProxyHandler = { 109 | get([node]: [Node], key: string | symbol): unknown { 110 | if (key === 'length') { 111 | consumeCollection(node) 112 | } 113 | 114 | return objectProxyHandler.get(node, key) 115 | }, 116 | 117 | ownKeys([node]: [Node]): ArrayLike { 118 | return objectProxyHandler.ownKeys(node) 119 | }, 120 | 121 | getOwnPropertyDescriptor( 122 | [node]: [Node], 123 | prop: string | symbol 124 | ): PropertyDescriptor | undefined { 125 | return objectProxyHandler.getOwnPropertyDescriptor(node, prop) 126 | }, 127 | 128 | has([node]: [Node], prop: string | symbol): boolean { 129 | return objectProxyHandler.has(node, prop) 130 | } 131 | } 132 | 133 | export function createNode | Record>( 134 | value: T 135 | ): Node { 136 | if (Array.isArray(value)) { 137 | return new ArrayTreeNode(value) 138 | } 139 | 140 | return new ObjectTreeNode(value) as Node 141 | } 142 | 143 | const keysMap = new WeakMap< 144 | Array | Record, 145 | Set 146 | >() 147 | 148 | export function updateNode | Record>( 149 | node: Node, 150 | newValue: T 151 | ): void { 152 | const { value, tags, children } = node 153 | 154 | node.value = newValue 155 | 156 | if ( 157 | Array.isArray(value) && 158 | Array.isArray(newValue) && 159 | value.length !== newValue.length 160 | ) { 161 | dirtyCollection(node) 162 | } else { 163 | if (value !== newValue) { 164 | let oldKeysSize = 0 165 | let newKeysSize = 0 166 | let anyKeysAdded = false 167 | 168 | for (const _key in value) { 169 | oldKeysSize++ 170 | } 171 | 172 | for (const key in newValue) { 173 | newKeysSize++ 174 | if (!(key in value)) { 175 | anyKeysAdded = true 176 | break 177 | } 178 | } 179 | 180 | const isDifferent = anyKeysAdded || oldKeysSize !== newKeysSize 181 | 182 | if (isDifferent) { 183 | dirtyCollection(node) 184 | } 185 | } 186 | } 187 | 188 | for (const key in tags) { 189 | const childValue = (value as Record)[key] 190 | const newChildValue = (newValue as Record)[key] 191 | 192 | if (childValue !== newChildValue) { 193 | dirtyCollection(node) 194 | dirtyTag(tags[key], newChildValue) 195 | } 196 | 197 | if (typeof newChildValue === 'object' && newChildValue !== null) { 198 | delete tags[key] 199 | } 200 | } 201 | 202 | for (const key in children) { 203 | const childNode = children[key] 204 | const newChildValue = (newValue as Record)[key] 205 | 206 | const childValue = childNode.value 207 | 208 | if (childValue === newChildValue) { 209 | continue 210 | } else if (typeof newChildValue === 'object' && newChildValue !== null) { 211 | updateNode(childNode, newChildValue as Record) 212 | } else { 213 | deleteNode(childNode) 214 | delete children[key] 215 | } 216 | } 217 | } 218 | 219 | function deleteNode(node: Node): void { 220 | if (node.tag) { 221 | dirtyTag(node.tag, null) 222 | } 223 | dirtyCollection(node) 224 | for (const key in node.tags) { 225 | dirtyTag(node.tags[key], null) 226 | } 227 | for (const key in node.children) { 228 | deleteNode(node.children[key]) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/autotrackMemoize/tracking.ts: -------------------------------------------------------------------------------- 1 | import type { Cell } from './autotracking' 2 | import { 3 | getValue as consumeTag, 4 | createCell as createStorage, 5 | setValue 6 | } from './autotracking' 7 | 8 | export type Tag = Cell 9 | 10 | const neverEq = (a: any, b: any): boolean => false 11 | 12 | export function createTag(): Tag { 13 | return createStorage(null, neverEq) 14 | } 15 | export { consumeTag } 16 | export function dirtyTag(tag: Tag, value: any): void { 17 | setValue(tag, value) 18 | } 19 | 20 | export interface Node< 21 | T extends Array | Record = 22 | | Array 23 | | Record 24 | > { 25 | collectionTag: Tag | null 26 | tag: Tag | null 27 | tags: Record 28 | children: Record 29 | proxy: T 30 | value: T 31 | id: number 32 | } 33 | 34 | export const consumeCollection = (node: Node): void => { 35 | let tag = node.collectionTag 36 | 37 | if (tag === null) { 38 | tag = node.collectionTag = createTag() 39 | } 40 | 41 | consumeTag(tag) 42 | } 43 | 44 | export const dirtyCollection = (node: Node): void => { 45 | const tag = node.collectionTag 46 | 47 | if (tag !== null) { 48 | dirtyTag(tag, null) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/autotrackMemoize/utils.ts: -------------------------------------------------------------------------------- 1 | export function assert( 2 | condition: any, 3 | msg = 'Assertion failed!' 4 | ): asserts condition { 5 | if (!condition) { 6 | console.error(msg) 7 | throw new Error(msg) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/devModeChecks/identityFunctionCheck.ts: -------------------------------------------------------------------------------- 1 | import type { AnyFunction } from '../types' 2 | 3 | /** 4 | * Runs a check to determine if the given result function behaves as an 5 | * identity function. An identity function is one that returns its 6 | * input unchanged, for example, `x => x`. This check helps ensure 7 | * efficient memoization and prevent unnecessary re-renders by encouraging 8 | * proper use of transformation logic in result functions and 9 | * extraction logic in input selectors. 10 | * 11 | * @param resultFunc - The result function to be checked. 12 | * @param inputSelectorsResults - The results of the input selectors. 13 | * @param outputSelectorResult - The result of the output selector. 14 | * 15 | * @see {@link https://reselect.js.org/api/development-only-stability-checks#identityfunctioncheck `identityFunctionCheck`} 16 | * 17 | * @since 5.0.0 18 | * @internal 19 | */ 20 | export const runIdentityFunctionCheck = ( 21 | resultFunc: AnyFunction, 22 | inputSelectorsResults: unknown[], 23 | outputSelectorResult: unknown 24 | ) => { 25 | if ( 26 | inputSelectorsResults.length === 1 && 27 | inputSelectorsResults[0] === outputSelectorResult 28 | ) { 29 | let isInputSameAsOutput = false 30 | try { 31 | const emptyObject = {} 32 | if (resultFunc(emptyObject) === emptyObject) isInputSameAsOutput = true 33 | } catch { 34 | // Do nothing 35 | } 36 | if (isInputSameAsOutput) { 37 | let stack: string | undefined = undefined 38 | try { 39 | throw new Error() 40 | } catch (e) { 41 | // eslint-disable-next-line @typescript-eslint/no-extra-semi, no-extra-semi 42 | ;({ stack } = e as Error) 43 | } 44 | console.warn( 45 | 'The result function returned its own inputs without modification. e.g' + 46 | '\n`createSelector([state => state.todos], todos => todos)`' + 47 | '\nThis could lead to inefficient memoization and unnecessary re-renders.' + 48 | '\nEnsure transformation logic is in the result function, and extraction logic is in the input selectors.', 49 | { stack } 50 | ) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/devModeChecks/inputStabilityCheck.ts: -------------------------------------------------------------------------------- 1 | import type { CreateSelectorOptions, UnknownMemoizer } from '../types' 2 | 3 | /** 4 | * Runs a stability check to ensure the input selector results remain stable 5 | * when provided with the same arguments. This function is designed to detect 6 | * changes in the output of input selectors, which can impact the performance of memoized selectors. 7 | * 8 | * @param inputSelectorResultsObject - An object containing two arrays: `inputSelectorResults` and `inputSelectorResultsCopy`, representing the results of input selectors. 9 | * @param options - Options object consisting of a `memoize` function and a `memoizeOptions` object. 10 | * @param inputSelectorArgs - List of arguments being passed to the input selectors. 11 | * 12 | * @see {@link https://reselect.js.org/api/development-only-stability-checks/#inputstabilitycheck `inputStabilityCheck`} 13 | * 14 | * @since 5.0.0 15 | * @internal 16 | */ 17 | export const runInputStabilityCheck = ( 18 | inputSelectorResultsObject: { 19 | inputSelectorResults: unknown[] 20 | inputSelectorResultsCopy: unknown[] 21 | }, 22 | options: Required< 23 | Pick< 24 | CreateSelectorOptions, 25 | 'memoize' | 'memoizeOptions' 26 | > 27 | >, 28 | inputSelectorArgs: unknown[] | IArguments 29 | ) => { 30 | const { memoize, memoizeOptions } = options 31 | const { inputSelectorResults, inputSelectorResultsCopy } = 32 | inputSelectorResultsObject 33 | const createAnEmptyObject = memoize(() => ({}), ...memoizeOptions) 34 | // if the memoize method thinks the parameters are equal, these *should* be the same reference 35 | const areInputSelectorResultsEqual = 36 | createAnEmptyObject.apply(null, inputSelectorResults) === 37 | createAnEmptyObject.apply(null, inputSelectorResultsCopy) 38 | if (!areInputSelectorResultsEqual) { 39 | let stack: string | undefined = undefined 40 | try { 41 | throw new Error() 42 | } catch (e) { 43 | // eslint-disable-next-line @typescript-eslint/no-extra-semi, no-extra-semi 44 | ;({ stack } = e as Error) 45 | } 46 | console.warn( 47 | 'An input selector returned a different result when passed same arguments.' + 48 | '\nThis means your output selector will likely run more frequently than intended.' + 49 | '\nAvoid returning a new reference inside your input selector, e.g.' + 50 | '\n`createSelector([state => state.todos.map(todo => todo.id)], todoIds => todoIds.length)`', 51 | { 52 | arguments: inputSelectorArgs, 53 | firstInputs: inputSelectorResults, 54 | secondInputs: inputSelectorResultsCopy, 55 | stack 56 | } 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/devModeChecks/setGlobalDevModeChecks.ts: -------------------------------------------------------------------------------- 1 | import type { DevModeChecks } from '../types' 2 | 3 | /** 4 | * Global configuration for development mode checks. This specifies the default 5 | * frequency at which each development mode check should be performed. 6 | * 7 | * @since 5.0.0 8 | * @internal 9 | */ 10 | export const globalDevModeChecks: DevModeChecks = { 11 | inputStabilityCheck: 'once', 12 | identityFunctionCheck: 'once' 13 | } 14 | 15 | /** 16 | * Overrides the development mode checks settings for all selectors. 17 | * 18 | * Reselect performs additional checks in development mode to help identify and 19 | * warn about potential issues in selector behavior. This function allows you to 20 | * customize the behavior of these checks across all selectors in your application. 21 | * 22 | * **Note**: This setting can still be overridden per selector inside `createSelector`'s `options` object. 23 | * See {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-identityfunctioncheck-option-directly-to-createselector per-selector-configuration} 24 | * and {@linkcode CreateSelectorOptions.identityFunctionCheck identityFunctionCheck} for more details. 25 | * 26 | * _The development mode checks do not run in production builds._ 27 | * 28 | * @param devModeChecks - An object specifying the desired settings for development mode checks. You can provide partial overrides. Unspecified settings will retain their current values. 29 | * 30 | * @example 31 | * ```ts 32 | * import { setGlobalDevModeChecks } from 'reselect' 33 | * import { DevModeChecks } from '../types' 34 | * 35 | * // Run only the first time the selector is called. (default) 36 | * setGlobalDevModeChecks({ inputStabilityCheck: 'once' }) 37 | * 38 | * // Run every time the selector is called. 39 | * setGlobalDevModeChecks({ inputStabilityCheck: 'always' }) 40 | * 41 | * // Never run the input stability check. 42 | * setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) 43 | * 44 | * // Run only the first time the selector is called. (default) 45 | * setGlobalDevModeChecks({ identityFunctionCheck: 'once' }) 46 | * 47 | * // Run every time the selector is called. 48 | * setGlobalDevModeChecks({ identityFunctionCheck: 'always' }) 49 | * 50 | * // Never run the identity function check. 51 | * setGlobalDevModeChecks({ identityFunctionCheck: 'never' }) 52 | * ``` 53 | * @see {@link https://reselect.js.org/api/development-only-stability-checks Development-Only Stability Checks} 54 | * @see {@link https://reselect.js.org/api/development-only-stability-checks#1-globally-through-setglobaldevmodechecks global-configuration} 55 | * 56 | * @since 5.0.0 57 | * @public 58 | */ 59 | export const setGlobalDevModeChecks = ( 60 | devModeChecks: Partial 61 | ) => { 62 | Object.assign(globalDevModeChecks, devModeChecks) 63 | } 64 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { autotrackMemoize as unstable_autotrackMemoize } from './autotrackMemoize/autotrackMemoize' 2 | export { createSelector, createSelectorCreator } from './createSelectorCreator' 3 | export type { CreateSelectorFunction } from './createSelectorCreator' 4 | export { createStructuredSelector } from './createStructuredSelector' 5 | export type { 6 | RootStateSelectors, 7 | SelectorResultsMap, 8 | SelectorsObject, 9 | StructuredSelectorCreator, 10 | TypedStructuredSelectorCreator 11 | } from './createStructuredSelector' 12 | export { setGlobalDevModeChecks } from './devModeChecks/setGlobalDevModeChecks' 13 | export { lruMemoize, referenceEqualityCheck } from './lruMemoize' 14 | export type { LruMemoizeOptions } from './lruMemoize' 15 | export type { 16 | Combiner, 17 | CreateSelectorOptions, 18 | DefaultMemoizeFields, 19 | DevModeCheckFrequency, 20 | DevModeChecks, 21 | DevModeChecksExecutionInfo, 22 | EqualityFn, 23 | ExtractMemoizerFields, 24 | GetParamsFromSelectors, 25 | GetStateFromSelectors, 26 | MemoizeOptionsFromParameters, 27 | OutputSelector, 28 | OutputSelectorFields, 29 | OverrideMemoizeOptions, 30 | Selector, 31 | SelectorArray, 32 | SelectorResultArray, 33 | UnknownMemoizer 34 | } from './types' 35 | export { weakMapMemoize } from './weakMapMemoize' 36 | export type { WeakMapMemoizeOptions } from './weakMapMemoize' 37 | -------------------------------------------------------------------------------- /src/lruMemoize.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AnyFunction, 3 | DefaultMemoizeFields, 4 | EqualityFn, 5 | Simplify 6 | } from './types' 7 | 8 | import type { NOT_FOUND_TYPE } from './utils' 9 | import { NOT_FOUND } from './utils' 10 | 11 | // Cache implementation based on Erik Rasmussen's `lru-memoize`: 12 | // https://github.com/erikras/lru-memoize 13 | 14 | interface Entry { 15 | key: unknown 16 | value: unknown 17 | } 18 | 19 | interface Cache { 20 | get(key: unknown): unknown | NOT_FOUND_TYPE 21 | put(key: unknown, value: unknown): void 22 | getEntries(): Entry[] 23 | clear(): void 24 | } 25 | 26 | function createSingletonCache(equals: EqualityFn): Cache { 27 | let entry: Entry | undefined 28 | return { 29 | get(key: unknown) { 30 | if (entry && equals(entry.key, key)) { 31 | return entry.value 32 | } 33 | 34 | return NOT_FOUND 35 | }, 36 | 37 | put(key: unknown, value: unknown) { 38 | entry = { key, value } 39 | }, 40 | 41 | getEntries() { 42 | return entry ? [entry] : [] 43 | }, 44 | 45 | clear() { 46 | entry = undefined 47 | } 48 | } 49 | } 50 | 51 | function createLruCache(maxSize: number, equals: EqualityFn): Cache { 52 | let entries: Entry[] = [] 53 | 54 | function get(key: unknown) { 55 | const cacheIndex = entries.findIndex(entry => equals(key, entry.key)) 56 | 57 | // We found a cached entry 58 | if (cacheIndex > -1) { 59 | const entry = entries[cacheIndex] 60 | 61 | // Cached entry not at top of cache, move it to the top 62 | if (cacheIndex > 0) { 63 | entries.splice(cacheIndex, 1) 64 | entries.unshift(entry) 65 | } 66 | 67 | return entry.value 68 | } 69 | 70 | // No entry found in cache, return sentinel 71 | return NOT_FOUND 72 | } 73 | 74 | function put(key: unknown, value: unknown) { 75 | if (get(key) === NOT_FOUND) { 76 | // TODO Is unshift slow? 77 | entries.unshift({ key, value }) 78 | if (entries.length > maxSize) { 79 | entries.pop() 80 | } 81 | } 82 | } 83 | 84 | function getEntries() { 85 | return entries 86 | } 87 | 88 | function clear() { 89 | entries = [] 90 | } 91 | 92 | return { get, put, getEntries, clear } 93 | } 94 | 95 | /** 96 | * Runs a simple reference equality check. 97 | * What {@linkcode lruMemoize lruMemoize} uses by default. 98 | * 99 | * **Note**: This function was previously known as `defaultEqualityCheck`. 100 | * 101 | * @public 102 | */ 103 | export const referenceEqualityCheck: EqualityFn = (a, b) => a === b 104 | 105 | export function createCacheKeyComparator(equalityCheck: EqualityFn) { 106 | return function areArgumentsShallowlyEqual( 107 | prev: unknown[] | IArguments | null, 108 | next: unknown[] | IArguments | null 109 | ): boolean { 110 | if (prev === null || next === null || prev.length !== next.length) { 111 | return false 112 | } 113 | 114 | // Do this in a for loop (and not a `forEach` or an `every`) so we can determine equality as fast as possible. 115 | const { length } = prev 116 | for (let i = 0; i < length; i++) { 117 | if (!equalityCheck(prev[i], next[i])) { 118 | return false 119 | } 120 | } 121 | 122 | return true 123 | } 124 | } 125 | 126 | /** 127 | * Options for configuring the behavior of a function memoized with 128 | * LRU (Least Recently Used) caching. 129 | * 130 | * @template Result - The type of the return value of the memoized function. 131 | * 132 | * @public 133 | */ 134 | export interface LruMemoizeOptions { 135 | /** 136 | * Function used to compare the individual arguments of the 137 | * provided calculation function. 138 | * 139 | * @default referenceEqualityCheck 140 | */ 141 | equalityCheck?: EqualityFn 142 | 143 | /** 144 | * If provided, used to compare a newly generated output value against 145 | * previous values in the cache. If a match is found, 146 | * the old value is returned. This addresses the common 147 | * ```ts 148 | * todos.map(todo => todo.id) 149 | * ``` 150 | * use case, where an update to another field in the original data causes 151 | * a recalculation due to changed references, but the output is still 152 | * effectively the same. 153 | * 154 | * @since 4.1.0 155 | */ 156 | resultEqualityCheck?: EqualityFn 157 | 158 | /** 159 | * The maximum size of the cache used by the selector. 160 | * A size greater than 1 means the selector will use an 161 | * LRU (Least Recently Used) cache, allowing for the caching of multiple 162 | * results based on different sets of arguments. 163 | * 164 | * @default 1 165 | */ 166 | maxSize?: number 167 | } 168 | 169 | /** 170 | * Creates a memoized version of a function with an optional 171 | * LRU (Least Recently Used) cache. The memoized function uses a cache to 172 | * store computed values. Depending on the `maxSize` option, it will use 173 | * either a singleton cache (for a single entry) or an 174 | * LRU cache (for multiple entries). 175 | * 176 | * **Note**: This function was previously known as `defaultMemoize`. 177 | * 178 | * @param func - The function to be memoized. 179 | * @param equalityCheckOrOptions - Either an equality check function or an options object. 180 | * @returns A memoized function with a `.clearCache()` method attached. 181 | * 182 | * @template Func - The type of the function that is memoized. 183 | * 184 | * @see {@link https://reselect.js.org/api/lruMemoize `lruMemoize`} 185 | * 186 | * @public 187 | */ 188 | export function lruMemoize( 189 | func: Func, 190 | equalityCheckOrOptions?: EqualityFn | LruMemoizeOptions> 191 | ) { 192 | const providedOptions = 193 | typeof equalityCheckOrOptions === 'object' 194 | ? equalityCheckOrOptions 195 | : { equalityCheck: equalityCheckOrOptions } 196 | 197 | const { 198 | equalityCheck = referenceEqualityCheck, 199 | maxSize = 1, 200 | resultEqualityCheck 201 | } = providedOptions 202 | 203 | const comparator = createCacheKeyComparator(equalityCheck) 204 | 205 | let resultsCount = 0 206 | 207 | const cache = 208 | maxSize <= 1 209 | ? createSingletonCache(comparator) 210 | : createLruCache(maxSize, comparator) 211 | 212 | function memoized() { 213 | let value = cache.get(arguments) as ReturnType 214 | if (value === NOT_FOUND) { 215 | // apply arguments instead of spreading for performance. 216 | // @ts-ignore 217 | value = func.apply(null, arguments) as ReturnType 218 | resultsCount++ 219 | 220 | if (resultEqualityCheck) { 221 | const entries = cache.getEntries() 222 | const matchingEntry = entries.find(entry => 223 | resultEqualityCheck(entry.value as ReturnType, value) 224 | ) 225 | 226 | if (matchingEntry) { 227 | value = matchingEntry.value as ReturnType 228 | resultsCount !== 0 && resultsCount-- 229 | } 230 | } 231 | 232 | cache.put(arguments, value) 233 | } 234 | return value 235 | } 236 | 237 | memoized.clearCache = () => { 238 | cache.clear() 239 | memoized.resetResultsCount() 240 | } 241 | 242 | memoized.resultsCount = () => resultsCount 243 | 244 | memoized.resetResultsCount = () => { 245 | resultsCount = 0 246 | } 247 | 248 | return memoized as Func & Simplify 249 | } 250 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { runIdentityFunctionCheck } from './devModeChecks/identityFunctionCheck' 2 | import { runInputStabilityCheck } from './devModeChecks/inputStabilityCheck' 3 | import { globalDevModeChecks } from './devModeChecks/setGlobalDevModeChecks' 4 | // eslint-disable-next-line @typescript-eslint/consistent-type-imports 5 | import type { 6 | DevModeChecks, 7 | Selector, 8 | SelectorArray, 9 | DevModeChecksExecutionInfo 10 | } from './types' 11 | 12 | export const NOT_FOUND = /* @__PURE__ */ Symbol('NOT_FOUND') 13 | export type NOT_FOUND_TYPE = typeof NOT_FOUND 14 | 15 | /** 16 | * Assert that the provided value is a function. If the assertion fails, 17 | * a `TypeError` is thrown with an optional custom error message. 18 | * 19 | * @param func - The value to be checked. 20 | * @param errorMessage - An optional custom error message to use if the assertion fails. 21 | * @throws A `TypeError` if the assertion fails. 22 | */ 23 | export function assertIsFunction( 24 | func: unknown, 25 | errorMessage = `expected a function, instead received ${typeof func}` 26 | ): asserts func is FunctionType { 27 | if (typeof func !== 'function') { 28 | throw new TypeError(errorMessage) 29 | } 30 | } 31 | 32 | /** 33 | * Assert that the provided value is an object. If the assertion fails, 34 | * a `TypeError` is thrown with an optional custom error message. 35 | * 36 | * @param object - The value to be checked. 37 | * @param errorMessage - An optional custom error message to use if the assertion fails. 38 | * @throws A `TypeError` if the assertion fails. 39 | */ 40 | export function assertIsObject>( 41 | object: unknown, 42 | errorMessage = `expected an object, instead received ${typeof object}` 43 | ): asserts object is ObjectType { 44 | if (typeof object !== 'object') { 45 | throw new TypeError(errorMessage) 46 | } 47 | } 48 | 49 | /** 50 | * Assert that the provided array is an array of functions. If the assertion fails, 51 | * a `TypeError` is thrown with an optional custom error message. 52 | * 53 | * @param array - The array to be checked. 54 | * @param errorMessage - An optional custom error message to use if the assertion fails. 55 | * @throws A `TypeError` if the assertion fails. 56 | */ 57 | export function assertIsArrayOfFunctions( 58 | array: unknown[], 59 | errorMessage = `expected all items to be functions, instead received the following types: ` 60 | ): asserts array is FunctionType[] { 61 | if ( 62 | !array.every((item): item is FunctionType => typeof item === 'function') 63 | ) { 64 | const itemTypes = array 65 | .map(item => 66 | typeof item === 'function' 67 | ? `function ${item.name || 'unnamed'}()` 68 | : typeof item 69 | ) 70 | .join(', ') 71 | throw new TypeError(`${errorMessage}[${itemTypes}]`) 72 | } 73 | } 74 | 75 | /** 76 | * Ensure that the input is an array. If it's already an array, it's returned as is. 77 | * If it's not an array, it will be wrapped in a new array. 78 | * 79 | * @param item - The item to be checked. 80 | * @returns An array containing the input item. If the input is already an array, it's returned without modification. 81 | */ 82 | export const ensureIsArray = (item: unknown) => { 83 | return Array.isArray(item) ? item : [item] 84 | } 85 | 86 | /** 87 | * Extracts the "dependencies" / "input selectors" from the arguments of `createSelector`. 88 | * 89 | * @param createSelectorArgs - Arguments passed to `createSelector` as an array. 90 | * @returns An array of "input selectors" / "dependencies". 91 | * @throws A `TypeError` if any of the input selectors is not function. 92 | */ 93 | export function getDependencies(createSelectorArgs: unknown[]) { 94 | const dependencies = Array.isArray(createSelectorArgs[0]) 95 | ? createSelectorArgs[0] 96 | : createSelectorArgs 97 | 98 | assertIsArrayOfFunctions( 99 | dependencies, 100 | `createSelector expects all input-selectors to be functions, but received the following types: ` 101 | ) 102 | 103 | return dependencies as SelectorArray 104 | } 105 | 106 | /** 107 | * Runs each input selector and returns their collective results as an array. 108 | * 109 | * @param dependencies - An array of "dependencies" or "input selectors". 110 | * @param inputSelectorArgs - An array of arguments being passed to the input selectors. 111 | * @returns An array of input selector results. 112 | */ 113 | export function collectInputSelectorResults( 114 | dependencies: SelectorArray, 115 | inputSelectorArgs: unknown[] | IArguments 116 | ) { 117 | const inputSelectorResults = [] 118 | const { length } = dependencies 119 | for (let i = 0; i < length; i++) { 120 | // @ts-ignore 121 | // apply arguments instead of spreading and mutate a local list of params for performance. 122 | inputSelectorResults.push(dependencies[i].apply(null, inputSelectorArgs)) 123 | } 124 | return inputSelectorResults 125 | } 126 | 127 | /** 128 | * Retrieves execution information for development mode checks. 129 | * 130 | * @param devModeChecks - Custom Settings for development mode checks. These settings will override the global defaults. 131 | * @param firstRun - Indicates whether it is the first time the selector has run. 132 | * @returns An object containing the execution information for each development mode check. 133 | */ 134 | export const getDevModeChecksExecutionInfo = ( 135 | firstRun: boolean, 136 | devModeChecks: Partial 137 | ) => { 138 | const { identityFunctionCheck, inputStabilityCheck } = { 139 | ...globalDevModeChecks, 140 | ...devModeChecks 141 | } 142 | return { 143 | identityFunctionCheck: { 144 | shouldRun: 145 | identityFunctionCheck === 'always' || 146 | (identityFunctionCheck === 'once' && firstRun), 147 | run: runIdentityFunctionCheck 148 | }, 149 | inputStabilityCheck: { 150 | shouldRun: 151 | inputStabilityCheck === 'always' || 152 | (inputStabilityCheck === 'once' && firstRun), 153 | run: runInputStabilityCheck 154 | } 155 | } satisfies DevModeChecksExecutionInfo 156 | } 157 | -------------------------------------------------------------------------------- /src/versionedTypes/index.ts: -------------------------------------------------------------------------------- 1 | export type { MergeParameters } from './ts47-mergeParameters' 2 | -------------------------------------------------------------------------------- /src/versionedTypes/ts47-mergeParameters.ts: -------------------------------------------------------------------------------- 1 | // This entire implementation courtesy of Anders Hjelsberg: 2 | // https://github.com/microsoft/TypeScript/pull/50831#issuecomment-1253830522 3 | 4 | import type { AnyFunction } from '../types' 5 | 6 | /** 7 | * Represents the longest array within an array of arrays. 8 | * 9 | * @template ArrayOfTuples An array of arrays. 10 | * 11 | * @internal 12 | */ 13 | type LongestTuple = 14 | ArrayOfTuples extends [infer FirstArray extends unknown[]] 15 | ? FirstArray 16 | : ArrayOfTuples extends [ 17 | infer FirstArray, 18 | ...infer RestArrays extends unknown[][] 19 | ] 20 | ? LongerOfTwo> 21 | : never 22 | 23 | /** 24 | * Determines the longer of two array types. 25 | * 26 | * @template ArrayOne First array type. 27 | * @template ArrayTwo Second array type. 28 | * 29 | * @internal 30 | */ 31 | type LongerOfTwo = keyof ArrayTwo extends keyof ArrayOne 32 | ? ArrayOne 33 | : ArrayTwo 34 | 35 | /** 36 | * Extracts the element at a specific index in an array. 37 | * 38 | * @template ArrayType The array type. 39 | * @template Index The index type. 40 | * 41 | * @internal 42 | */ 43 | type ElementAt< 44 | ArrayType extends unknown[], 45 | Index extends PropertyKey 46 | > = Index extends keyof ArrayType ? ArrayType[Index] : unknown 47 | 48 | /** 49 | * Maps each array in an array of arrays to its element at a given index. 50 | * 51 | * @template ArrayOfTuples An array of arrays. 52 | * @template Index The index to extract from each array. 53 | * 54 | * @internal 55 | */ 56 | type ElementsAtGivenIndex< 57 | ArrayOfTuples extends readonly unknown[][], 58 | Index extends PropertyKey 59 | > = { 60 | [ArrayIndex in keyof ArrayOfTuples]: ElementAt< 61 | ArrayOfTuples[ArrayIndex], 62 | Index 63 | > 64 | } 65 | 66 | /** 67 | * Computes the intersection of all types in a tuple. 68 | * 69 | * @template Tuple A tuple of types. 70 | * 71 | * @internal 72 | */ 73 | type Intersect = Tuple extends [] 74 | ? unknown 75 | : Tuple extends [infer Head, ...infer Tail] 76 | ? Head & Intersect 77 | : Tuple[number] 78 | 79 | /** 80 | * Merges a tuple of arrays into a single tuple, intersecting types at each index. 81 | * 82 | * @template ArrayOfTuples An array of tuples. 83 | * @template LongestArray The longest array in ArrayOfTuples. 84 | * 85 | * @internal 86 | */ 87 | type MergeTuples< 88 | ArrayOfTuples extends readonly unknown[][], 89 | LongestArray extends unknown[] = LongestTuple 90 | > = { 91 | [Index in keyof LongestArray]: Intersect< 92 | ElementsAtGivenIndex 93 | > 94 | } 95 | 96 | /** 97 | * Extracts the parameter types from a tuple of functions. 98 | * 99 | * @template FunctionsArray An array of function types. 100 | * 101 | * @internal 102 | */ 103 | type ExtractParameters = { 104 | [Index in keyof FunctionsArray]: Parameters 105 | } 106 | 107 | /** 108 | * Merges the parameters of a tuple of functions into a single tuple. 109 | * 110 | * @template FunctionsArray An array of function types. 111 | * 112 | * @internal 113 | */ 114 | export type MergeParameters = 115 | '0' extends keyof FunctionsArray 116 | ? MergeTuples> 117 | : Parameters 118 | -------------------------------------------------------------------------------- /test/autotrackMemoize.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | unstable_autotrackMemoize as autotrackMemoize, 3 | createSelectorCreator 4 | } from 'reselect' 5 | import { setEnvToProd } from './testUtils' 6 | 7 | // Construct 1E6 states for perf test outside of the perf test so as to not change the execute time of the test function 8 | const numOfStates = 1_000_000 9 | interface StateA { 10 | a: number 11 | } 12 | 13 | interface StateAB { 14 | a: number 15 | b: number 16 | } 17 | 18 | interface StateSub { 19 | sub: { 20 | a: number 21 | } 22 | } 23 | 24 | const states: StateAB[] = [] 25 | 26 | for (let i = 0; i < numOfStates; i++) { 27 | states.push({ a: 1, b: 2 }) 28 | } 29 | 30 | describe('Basic selector behavior with autotrack', () => { 31 | const createSelector = createSelectorCreator(autotrackMemoize) 32 | 33 | test('basic selector', () => { 34 | // console.log('Selector test') 35 | const selector = createSelector( 36 | (state: StateA) => state.a, 37 | a => a, 38 | { devModeChecks: { identityFunctionCheck: 'never' } } 39 | ) 40 | const firstState = { a: 1 } 41 | const firstStateNewPointer = { a: 1 } 42 | const secondState = { a: 2 } 43 | 44 | expect(selector(firstState)).toBe(1) 45 | expect(selector(firstState)).toBe(1) 46 | expect(selector.recomputations()).toBe(1) 47 | expect(selector(firstStateNewPointer)).toBe(1) 48 | expect(selector.recomputations()).toBe(1) 49 | expect(selector(secondState)).toBe(2) 50 | expect(selector.recomputations()).toBe(2) 51 | }) 52 | 53 | test("don't pass extra parameters to inputSelector when only called with the state", () => { 54 | const selector = createSelector( 55 | (...params: any[]) => params.length, 56 | a => a, 57 | { devModeChecks: { identityFunctionCheck: 'never' } } 58 | ) 59 | expect(selector({})).toBe(1) 60 | }) 61 | 62 | test('basic selector multiple keys', () => { 63 | const selector = createSelector( 64 | (state: StateAB) => state.a, 65 | (state: StateAB) => state.b, 66 | (a, b) => a + b 67 | ) 68 | const state1 = { a: 1, b: 2 } 69 | expect(selector(state1)).toBe(3) 70 | expect(selector(state1)).toBe(3) 71 | expect(selector.recomputations()).toBe(1) 72 | const state2 = { a: 3, b: 2 } 73 | expect(selector(state2)).toBe(5) 74 | expect(selector(state2)).toBe(5) 75 | expect(selector.recomputations()).toBe(2) 76 | }) 77 | 78 | test('basic selector invalid input selector', () => { 79 | expect(() => 80 | createSelector( 81 | // @ts-ignore 82 | (state: StateAB) => state.a, 83 | function input2(state: StateAB) { 84 | return state.b 85 | }, 86 | 'not a function', 87 | (a: any, b: any) => a + b 88 | ) 89 | ).toThrow( 90 | 'createSelector expects all input-selectors to be functions, but received the following types: [function unnamed(), function input2(), string]' 91 | ) 92 | 93 | expect(() => 94 | // @ts-ignore 95 | createSelector((state: StateAB) => state.a, 'not a function') 96 | ).toThrow( 97 | 'createSelector expects an output function after the inputs, but received: [string]' 98 | ) 99 | }) 100 | 101 | const isCoverage = process.env.COVERAGE 102 | 103 | // don't run performance tests for coverage 104 | describe.skipIf(isCoverage)('performance checks', () => { 105 | beforeEach(setEnvToProd) 106 | 107 | test('basic selector cache hit performance', () => { 108 | const selector = createSelector( 109 | (state: StateAB) => state.a, 110 | (state: StateAB) => state.b, 111 | (a, b) => a + b, 112 | { devModeChecks: { identityFunctionCheck: 'never' } } 113 | ) 114 | const state1 = { a: 1, b: 2 } 115 | 116 | const start = performance.now() 117 | for (let i = 0; i < 1_000_000; i++) { 118 | selector(state1) 119 | } 120 | const totalTime = performance.now() - start 121 | 122 | expect(selector(state1)).toBe(3) 123 | expect(selector.recomputations()).toBe(1) 124 | // Expected a million calls to a selector with the same arguments to take less than 2 seconds 125 | expect(totalTime).toBeLessThan(2000) 126 | }) 127 | 128 | test('basic selector cache hit performance for state changes but shallowly equal selector args', () => { 129 | const selector = createSelector( 130 | (state: StateAB) => state.a, 131 | (state: StateAB) => state.b, 132 | (a, b) => a + b, 133 | { devModeChecks: { identityFunctionCheck: 'never' } } 134 | ) 135 | 136 | const start = performance.now() 137 | for (let i = 0; i < 1_000_000; i++) { 138 | selector(states[i]) 139 | } 140 | const totalTime = performance.now() - start 141 | 142 | expect(selector(states[0])).toBe(3) 143 | expect(selector.recomputations()).toBe(1) 144 | 145 | // Expected a million calls to a selector with the same arguments to take less than 1 second 146 | expect(totalTime).toBeLessThan(2000) 147 | }) 148 | }) 149 | 150 | test('memoized composite arguments', () => { 151 | const selector = createSelector( 152 | (state: StateSub) => state.sub, 153 | sub => sub.a 154 | ) 155 | const state1 = { sub: { a: 1 } } 156 | expect(selector(state1)).toEqual(1) 157 | expect(selector(state1)).toEqual(1) 158 | expect(selector.recomputations()).toBe(1) 159 | const state2 = { sub: { a: 2 } } 160 | expect(selector(state2)).toEqual(2) 161 | expect(selector.recomputations()).toBe(2) 162 | }) 163 | 164 | test('first argument can be an array', () => { 165 | const selector = createSelector( 166 | [state => state.a, state => state.b], 167 | (a, b) => { 168 | return a + b 169 | } 170 | ) 171 | expect(selector({ a: 1, b: 2 })).toBe(3) 172 | expect(selector({ a: 1, b: 2 })).toBe(3) 173 | expect(selector.recomputations()).toBe(1) 174 | expect(selector({ a: 3, b: 2 })).toBe(5) 175 | expect(selector.recomputations()).toBe(2) 176 | }) 177 | 178 | test('can accept props', () => { 179 | let called = 0 180 | const selector = createSelector( 181 | (state: StateAB) => state.a, 182 | (state: StateAB) => state.b, 183 | (state: StateAB, props: { c: number }) => props.c, 184 | (a, b, c) => { 185 | called++ 186 | return a + b + c 187 | } 188 | ) 189 | expect(selector({ a: 1, b: 2 }, { c: 100 })).toBe(103) 190 | }) 191 | 192 | test('recomputes result after exception', () => { 193 | let called = 0 194 | const selector = createSelector( 195 | (state: StateA) => state.a, 196 | () => { 197 | called++ 198 | throw Error('test error') 199 | }, 200 | { devModeChecks: { identityFunctionCheck: 'never' } } 201 | ) 202 | expect(() => selector({ a: 1 })).toThrow('test error') 203 | expect(() => selector({ a: 1 })).toThrow('test error') 204 | expect(called).toBe(2) 205 | }) 206 | 207 | test('memoizes previous result before exception', () => { 208 | let called = 0 209 | const selector = createSelector( 210 | (state: StateA) => state.a, 211 | a => { 212 | called++ 213 | if (a > 1) throw Error('test error') 214 | return a 215 | }, 216 | { devModeChecks: { identityFunctionCheck: 'never' } } 217 | ) 218 | const state1 = { a: 1 } 219 | const state2 = { a: 2 } 220 | expect(selector(state1)).toBe(1) 221 | expect(() => selector(state2)).toThrow('test error') 222 | expect(selector(state1)).toBe(1) 223 | expect(called).toBe(2) 224 | }) 225 | }) 226 | -------------------------------------------------------------------------------- /test/benchmarks/orderOfExecution.bench.ts: -------------------------------------------------------------------------------- 1 | import type { OutputSelector, Selector } from 'reselect' 2 | import { createSelector, lruMemoize } from 'reselect' 3 | import type { Options } from 'tinybench' 4 | import { bench } from 'vitest' 5 | import type { RootState } from '../testUtils' 6 | import { 7 | countRecomputations, 8 | expensiveComputation, 9 | logFunctionInfo, 10 | logSelectorRecomputations, 11 | resetSelector, 12 | runMultipleTimes, 13 | setFunctionNames, 14 | setupStore, 15 | toggleCompleted, 16 | toggleRead 17 | } from '../testUtils' 18 | 19 | describe('Less vs more computation in input selectors', () => { 20 | const store = setupStore() 21 | const runSelector = (selector: Selector) => { 22 | runMultipleTimes(selector, 100, store.getState()) 23 | } 24 | const selectorLessInInput = createSelector( 25 | [(state: RootState) => state.todos], 26 | todos => { 27 | expensiveComputation() 28 | return todos.filter(todo => todo.completed) 29 | } 30 | ) 31 | const selectorMoreInInput = createSelector( 32 | [ 33 | (state: RootState) => { 34 | expensiveComputation() 35 | return state.todos 36 | } 37 | ], 38 | todos => todos.filter(todo => todo.completed) 39 | ) 40 | 41 | const nonMemoized = countRecomputations((state: RootState) => { 42 | expensiveComputation() 43 | return state.todos.filter(todo => todo.completed) 44 | }) 45 | const commonOptions: Options = { 46 | iterations: 10, 47 | time: 0 48 | } 49 | setFunctionNames({ selectorLessInInput, selectorMoreInInput, nonMemoized }) 50 | const createOptions = ( 51 | selector: S, 52 | commonOptions: Options = {} 53 | ) => { 54 | const options: Options = { 55 | setup: (task, mode) => { 56 | if (mode === 'warmup') return 57 | task.opts = { 58 | beforeEach: () => { 59 | store.dispatch(toggleRead(1)) 60 | }, 61 | afterAll: () => { 62 | logSelectorRecomputations(selector) 63 | } 64 | } 65 | } 66 | } 67 | return { ...commonOptions, ...options } 68 | } 69 | bench( 70 | selectorLessInInput, 71 | () => { 72 | runSelector(selectorLessInInput) 73 | }, 74 | createOptions(selectorLessInInput, commonOptions) 75 | ) 76 | bench( 77 | selectorMoreInInput, 78 | () => { 79 | runSelector(selectorMoreInInput) 80 | }, 81 | createOptions(selectorMoreInInput, commonOptions) 82 | ) 83 | bench( 84 | nonMemoized, 85 | () => { 86 | runSelector(nonMemoized) 87 | }, 88 | { 89 | ...commonOptions, 90 | setup: (task, mode) => { 91 | if (mode === 'warmup') return 92 | nonMemoized.resetRecomputations() 93 | task.opts = { 94 | beforeEach: () => { 95 | store.dispatch(toggleCompleted(1)) 96 | }, 97 | afterAll: () => { 98 | logFunctionInfo(nonMemoized, nonMemoized.recomputations()) 99 | } 100 | } 101 | } 102 | } 103 | ) 104 | }) 105 | 106 | // This benchmark is made to test to see at what point it becomes beneficial 107 | // to use reselect to memoize a function that is a plain field accessor. 108 | describe('Reselect vs standalone memoization for field access', () => { 109 | const store = setupStore() 110 | const runSelector = (selector: Selector) => { 111 | runMultipleTimes(selector, 1_000_000, store.getState()) 112 | } 113 | const commonOptions: Options = { 114 | // warmupIterations: 0, 115 | // warmupTime: 0, 116 | // iterations: 10, 117 | // time: 0 118 | } 119 | const fieldAccessorWithReselect = createSelector( 120 | [(state: RootState) => state.users], 121 | users => users.appSettings 122 | ) 123 | const fieldAccessorWithMemoize = countRecomputations( 124 | lruMemoize((state: RootState) => { 125 | return state.users.appSettings 126 | }) 127 | ) 128 | const nonMemoizedAccessor = countRecomputations( 129 | (state: RootState) => state.users.appSettings 130 | ) 131 | 132 | setFunctionNames({ 133 | fieldAccessorWithReselect, 134 | fieldAccessorWithMemoize, 135 | nonMemoizedAccessor 136 | }) 137 | const createOptions = ( 138 | selector: S, 139 | commonOptions: Options = {} 140 | ) => { 141 | const options: Options = { 142 | setup: (task, mode) => { 143 | if (mode === 'warmup') return 144 | resetSelector(selector) 145 | task.opts = { 146 | beforeEach: () => { 147 | store.dispatch(toggleCompleted(1)) 148 | }, 149 | afterAll: () => { 150 | logSelectorRecomputations(selector) 151 | } 152 | } 153 | } 154 | } 155 | return { ...commonOptions, ...options } 156 | } 157 | bench( 158 | fieldAccessorWithReselect, 159 | () => { 160 | runSelector(fieldAccessorWithReselect) 161 | }, 162 | createOptions(fieldAccessorWithReselect, commonOptions) 163 | ) 164 | bench( 165 | fieldAccessorWithMemoize, 166 | () => { 167 | runSelector(fieldAccessorWithMemoize) 168 | }, 169 | { 170 | ...commonOptions, 171 | setup: (task, mode) => { 172 | if (mode === 'warmup') return 173 | fieldAccessorWithMemoize.resetRecomputations() 174 | fieldAccessorWithMemoize.clearCache() 175 | task.opts = { 176 | beforeEach: () => { 177 | store.dispatch(toggleCompleted(1)) 178 | }, 179 | afterAll: () => { 180 | logFunctionInfo( 181 | fieldAccessorWithMemoize, 182 | fieldAccessorWithMemoize.recomputations() 183 | ) 184 | } 185 | } 186 | } 187 | } 188 | ) 189 | bench( 190 | nonMemoizedAccessor, 191 | () => { 192 | runSelector(nonMemoizedAccessor) 193 | }, 194 | { 195 | ...commonOptions, 196 | setup: (task, mode) => { 197 | if (mode === 'warmup') return 198 | nonMemoizedAccessor.resetRecomputations() 199 | task.opts = { 200 | beforeEach: () => { 201 | store.dispatch(toggleCompleted(1)) 202 | }, 203 | afterAll: () => { 204 | logFunctionInfo( 205 | nonMemoizedAccessor, 206 | nonMemoizedAccessor.recomputations() 207 | ) 208 | } 209 | } 210 | } 211 | } 212 | ) 213 | }) 214 | -------------------------------------------------------------------------------- /test/benchmarks/resultEqualityCheck.bench.ts: -------------------------------------------------------------------------------- 1 | import type { AnyFunction } from '@internal/types' 2 | import type { OutputSelector, Selector } from 'reselect' 3 | import { 4 | createSelector, 5 | lruMemoize, 6 | referenceEqualityCheck, 7 | weakMapMemoize 8 | } from 'reselect' 9 | import type { Options } from 'tinybench' 10 | import { bench } from 'vitest' 11 | import { 12 | logSelectorRecomputations, 13 | setFunctionNames, 14 | setupStore, 15 | toggleCompleted, 16 | type RootState 17 | } from '../testUtils' 18 | 19 | describe('memoize functions performance with resultEqualityCheck set to referenceEqualityCheck vs. without resultEqualityCheck', () => { 20 | describe('comparing selectors created with createSelector', () => { 21 | const store = setupStore() 22 | 23 | const arrayOfNumbers = Array.from({ length: 1_000 }, (num, index) => index) 24 | 25 | const commonOptions: Options = { 26 | iterations: 10_000, 27 | time: 0 28 | } 29 | 30 | const runSelector = (selector: S) => { 31 | arrayOfNumbers.forEach(num => { 32 | selector(store.getState()) 33 | }) 34 | } 35 | 36 | const createAppSelector = createSelector.withTypes() 37 | 38 | const selectTodoIdsWeakMap = createAppSelector( 39 | [state => state.todos], 40 | todos => todos.map(({ id }) => id) 41 | ) 42 | 43 | const selectTodoIdsWeakMapWithResultEqualityCheck = createAppSelector( 44 | [state => state.todos], 45 | todos => todos.map(({ id }) => id), 46 | { 47 | memoizeOptions: { resultEqualityCheck: referenceEqualityCheck }, 48 | argsMemoizeOptions: { resultEqualityCheck: referenceEqualityCheck } 49 | } 50 | ) 51 | 52 | const selectTodoIdsLru = createAppSelector( 53 | [state => state.todos], 54 | todos => todos.map(({ id }) => id), 55 | { memoize: lruMemoize, argsMemoize: lruMemoize } 56 | ) 57 | 58 | const selectTodoIdsLruWithResultEqualityCheck = createAppSelector( 59 | [state => state.todos], 60 | todos => todos.map(({ id }) => id), 61 | { 62 | memoize: lruMemoize, 63 | memoizeOptions: { resultEqualityCheck: referenceEqualityCheck }, 64 | argsMemoize: lruMemoize, 65 | argsMemoizeOptions: { resultEqualityCheck: referenceEqualityCheck } 66 | } 67 | ) 68 | 69 | const selectors = { 70 | selectTodoIdsWeakMap, 71 | selectTodoIdsWeakMapWithResultEqualityCheck, 72 | selectTodoIdsLru, 73 | selectTodoIdsLruWithResultEqualityCheck 74 | } 75 | 76 | setFunctionNames(selectors) 77 | 78 | const createOptions = (selector: S) => { 79 | const options: Options = { 80 | setup: (task, mode) => { 81 | if (mode === 'warmup') return 82 | 83 | task.opts = { 84 | beforeEach: () => { 85 | store.dispatch(toggleCompleted(1)) 86 | }, 87 | 88 | afterAll: () => { 89 | logSelectorRecomputations(selector) 90 | } 91 | } 92 | } 93 | } 94 | return { ...commonOptions, ...options } 95 | } 96 | 97 | Object.values(selectors).forEach(selector => { 98 | bench( 99 | selector, 100 | () => { 101 | runSelector(selector) 102 | }, 103 | createOptions(selector) 104 | ) 105 | }) 106 | }) 107 | 108 | describe('comparing selectors created with memoize functions', () => { 109 | const store = setupStore() 110 | 111 | const arrayOfNumbers = Array.from( 112 | { length: 100_000 }, 113 | (num, index) => index 114 | ) 115 | 116 | const commonOptions: Options = { 117 | iterations: 1000, 118 | time: 0 119 | } 120 | 121 | const runSelector = (selector: S) => { 122 | arrayOfNumbers.forEach(num => { 123 | selector(store.getState()) 124 | }) 125 | } 126 | 127 | const selectTodoIdsWeakMap = weakMapMemoize((state: RootState) => 128 | state.todos.map(({ id }) => id) 129 | ) 130 | 131 | const selectTodoIdsWeakMapWithResultEqualityCheck = weakMapMemoize( 132 | (state: RootState) => state.todos.map(({ id }) => id), 133 | { resultEqualityCheck: referenceEqualityCheck } 134 | ) 135 | 136 | const selectTodoIdsLru = lruMemoize((state: RootState) => 137 | state.todos.map(({ id }) => id) 138 | ) 139 | 140 | const selectTodoIdsLruWithResultEqualityCheck = lruMemoize( 141 | (state: RootState) => state.todos.map(({ id }) => id), 142 | { resultEqualityCheck: referenceEqualityCheck } 143 | ) 144 | 145 | const memoizedFunctions = { 146 | selectTodoIdsWeakMap, 147 | selectTodoIdsWeakMapWithResultEqualityCheck, 148 | selectTodoIdsLru, 149 | selectTodoIdsLruWithResultEqualityCheck 150 | } 151 | 152 | setFunctionNames(memoizedFunctions) 153 | 154 | const createOptions = < 155 | Func extends AnyFunction & { resultsCount: () => number } 156 | >( 157 | memoizedFunction: Func 158 | ) => { 159 | const options: Options = { 160 | setup: (task, mode) => { 161 | if (mode === 'warmup') return 162 | 163 | task.opts = { 164 | beforeEach: () => { 165 | store.dispatch(toggleCompleted(1)) 166 | }, 167 | 168 | afterAll: () => { 169 | console.log( 170 | memoizedFunction.name, 171 | memoizedFunction.resultsCount() 172 | ) 173 | } 174 | } 175 | } 176 | } 177 | return { ...commonOptions, ...options } 178 | } 179 | 180 | Object.values(memoizedFunctions).forEach(memoizedFunction => { 181 | bench( 182 | memoizedFunction, 183 | () => { 184 | runSelector(memoizedFunction) 185 | }, 186 | createOptions(memoizedFunction) 187 | ) 188 | }) 189 | }) 190 | }) 191 | -------------------------------------------------------------------------------- /test/createSelector.withTypes.test.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import type { RootState } from './testUtils' 3 | import { localTest } from './testUtils' 4 | 5 | describe(createSelector.withTypes, () => { 6 | const createTypedSelector = createSelector.withTypes() 7 | 8 | localTest('should return createSelector', ({ state }) => { 9 | expect(createTypedSelector.withTypes).to.be.a('function') 10 | 11 | expect(createTypedSelector.withTypes().withTypes).to.be.a('function') 12 | 13 | expect(createTypedSelector).toBe(createSelector) 14 | 15 | const selectTodoIds = createTypedSelector([state => state.todos], todos => 16 | todos.map(({ id }) => id) 17 | ) 18 | 19 | expect(selectTodoIds).toBeMemoizedSelector() 20 | 21 | expect(selectTodoIds(state)).to.be.an('array').that.is.not.empty 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /test/createStructuredSelector.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createSelector, 3 | createSelectorCreator, 4 | createStructuredSelector, 5 | lruMemoize 6 | } from 'reselect' 7 | import type { LocalTestContext, RootState } from './testUtils' 8 | import { setupStore } from './testUtils' 9 | 10 | interface StateAB { 11 | a: number 12 | b: number 13 | } 14 | 15 | describe(createStructuredSelector, () => { 16 | test('structured selector', () => { 17 | const selector = createStructuredSelector({ 18 | x: (state: StateAB) => state.a, 19 | y: (state: StateAB) => state.b 20 | }) 21 | const firstResult = selector({ a: 1, b: 2 }) 22 | expect(firstResult).toEqual({ x: 1, y: 2 }) 23 | expect(selector({ a: 1, b: 2 })).toBe(firstResult) 24 | const secondResult = selector({ a: 2, b: 2 }) 25 | expect(secondResult).toEqual({ x: 2, y: 2 }) 26 | expect(selector({ a: 2, b: 2 })).toBe(secondResult) 27 | }) 28 | 29 | test('structured selector with invalid arguments', () => { 30 | expect(() => 31 | createStructuredSelector( 32 | // @ts-expect-error 33 | (state: StateAB) => state.a, 34 | (state: StateAB) => state.b 35 | ) 36 | ).toThrow(/expects first argument to be an object.*function/) 37 | expect(() => 38 | createStructuredSelector({ 39 | a: state => state.b, 40 | // @ts-expect-error 41 | c: 'd' 42 | }) 43 | ).toThrow( 44 | 'createSelector expects all input-selectors to be functions, but received the following types: [function a(), string]' 45 | ) 46 | }) 47 | 48 | test('structured selector with custom selector creator', () => { 49 | const customSelectorCreator = createSelectorCreator( 50 | lruMemoize, 51 | (a, b) => a === b 52 | ) 53 | const selector = createStructuredSelector( 54 | { 55 | x: (state: StateAB) => state.a, 56 | y: (state: StateAB) => state.b 57 | }, 58 | customSelectorCreator 59 | ) 60 | const firstResult = selector({ a: 1, b: 2 }) 61 | expect(firstResult).toEqual({ x: 1, y: 2 }) 62 | expect(selector({ a: 1, b: 2 })).toBe(firstResult) 63 | expect(selector({ a: 2, b: 2 })).toEqual({ x: 2, y: 2 }) 64 | }) 65 | }) 66 | 67 | describe('structured selector created with createStructuredSelector', localTest => { 68 | beforeEach(context => { 69 | const store = setupStore() 70 | context.store = store 71 | context.state = store.getState() 72 | }) 73 | localTest( 74 | 'structured selector created with createStructuredSelector and createSelector are the same', 75 | ({ state }) => { 76 | const structuredSelector = createStructuredSelector( 77 | { 78 | allTodos: (state: RootState) => state.todos, 79 | allAlerts: (state: RootState) => state.alerts, 80 | selectedTodo: (state: RootState, id: number) => state.todos[id] 81 | }, 82 | createSelector 83 | ) 84 | const selector = createSelector( 85 | [ 86 | (state: RootState) => state.todos, 87 | (state: RootState) => state.alerts, 88 | (state: RootState, id: number) => state.todos[id] 89 | ], 90 | (allTodos, allAlerts, selectedTodo) => { 91 | return { 92 | allTodos, 93 | allAlerts, 94 | selectedTodo 95 | } 96 | } 97 | ) 98 | expect(selector(state, 1).selectedTodo.id).toBe( 99 | structuredSelector(state, 1).selectedTodo.id 100 | ) 101 | expect(structuredSelector.dependencies) 102 | .to.be.an('array') 103 | .with.lengthOf(selector.dependencies.length) 104 | expect( 105 | structuredSelector.resultFunc(state.todos, state.alerts, state.todos[0]) 106 | ).toStrictEqual( 107 | selector.resultFunc(state.todos, state.alerts, state.todos[0]) 108 | ) 109 | expect( 110 | structuredSelector.memoizedResultFunc( 111 | state.todos, 112 | state.alerts, 113 | state.todos[0] 114 | ) 115 | ).toStrictEqual( 116 | selector.memoizedResultFunc(state.todos, state.alerts, state.todos[0]) 117 | ) 118 | expect(structuredSelector.argsMemoize).toBe(selector.argsMemoize) 119 | expect(structuredSelector.memoize).toBe(selector.memoize) 120 | expect(structuredSelector.recomputations()).toBe( 121 | selector.recomputations() 122 | ) 123 | expect(structuredSelector.lastResult()).toStrictEqual( 124 | selector.lastResult() 125 | ) 126 | expect(Object.keys(structuredSelector)).toStrictEqual( 127 | Object.keys(selector) 128 | ) 129 | } 130 | ) 131 | 132 | localTest( 133 | 'structured selector invalid args can throw runtime errors', 134 | ({ state }) => { 135 | const structuredSelector = createStructuredSelector( 136 | { 137 | allTodos: (state: RootState) => state.todos, 138 | allAlerts: (state: RootState) => state.alerts, 139 | selectedTodo: ( 140 | state: RootState, 141 | id: number, 142 | field: keyof RootState['todos'][number] 143 | ) => state.todos[id][field] 144 | }, 145 | createSelector 146 | ) 147 | const selector = createSelector( 148 | [ 149 | (state: RootState) => state.todos, 150 | (state: RootState) => state.alerts, 151 | ( 152 | state: RootState, 153 | id: number, 154 | field: keyof RootState['todos'][number] 155 | ) => state.todos[id][field] 156 | ], 157 | (allTodos, allAlerts, selectedTodo) => { 158 | return { 159 | allTodos, 160 | allAlerts, 161 | selectedTodo 162 | } 163 | } 164 | ) 165 | // These two cases are the same. 166 | // @ts-expect-error 167 | expect(() => structuredSelector(state)).toThrowError(TypeError) 168 | // @ts-expect-error 169 | expect(() => selector(state)).toThrowError(TypeError) 170 | } 171 | ) 172 | }) 173 | -------------------------------------------------------------------------------- /test/createStructuredSelector.withTypes.test.ts: -------------------------------------------------------------------------------- 1 | import { createStructuredSelector } from 'reselect' 2 | import type { RootState } from './testUtils' 3 | import { localTest } from './testUtils' 4 | 5 | describe(createStructuredSelector.withTypes, () => { 6 | const createTypedStructuredSelector = 7 | createStructuredSelector.withTypes() 8 | 9 | localTest('should return createStructuredSelector', ({ state }) => { 10 | expect(createTypedStructuredSelector.withTypes).to.be.a('function') 11 | 12 | expect(createTypedStructuredSelector.withTypes().withTypes).to.be.a( 13 | 'function' 14 | ) 15 | 16 | expect(createTypedStructuredSelector).toBe(createStructuredSelector) 17 | 18 | const structuredSelector = createTypedStructuredSelector({ 19 | todos: state => state.todos, 20 | alerts: state => state.alerts 21 | }) 22 | 23 | expect(structuredSelector).toBeMemoizedSelector() 24 | 25 | expect(structuredSelector(state)).to.be.an('object').that.is.not.empty 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/customMatchers.d.ts: -------------------------------------------------------------------------------- 1 | import type { Assertion, AsymmetricMatchersContaining } from 'vitest' 2 | 3 | interface CustomMatchers { 4 | toBeMemoizedSelector(): R 5 | } 6 | 7 | declare module 'vitest' { 8 | interface Assertion extends CustomMatchers {} 9 | interface AsymmetricMatchersContaining extends CustomMatchers {} 10 | } 11 | -------------------------------------------------------------------------------- /test/selectorUtils.spec.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, lruMemoize } from 'reselect' 2 | import type { StateA, StateAB } from 'testTypes' 3 | 4 | describe('createSelector exposed utils', () => { 5 | test('resetRecomputations', () => { 6 | const selector = createSelector( 7 | (state: StateA) => state.a, 8 | a => a, 9 | { 10 | memoize: lruMemoize, 11 | argsMemoize: lruMemoize, 12 | devModeChecks: { identityFunctionCheck: 'never' } 13 | } 14 | ) 15 | expect(selector({ a: 1 })).toBe(1) 16 | expect(selector({ a: 1 })).toBe(1) 17 | expect(selector.recomputations()).toBe(1) 18 | expect(selector({ a: 2 })).toBe(2) 19 | expect(selector.recomputations()).toBe(2) 20 | 21 | selector.resetRecomputations() 22 | expect(selector.recomputations()).toBe(0) 23 | 24 | expect(selector({ a: 1 })).toBe(1) 25 | expect(selector({ a: 1 })).toBe(1) 26 | expect(selector.recomputations()).toBe(1) 27 | expect(selector({ a: 2 })).toBe(2) 28 | expect(selector.recomputations()).toBe(2) 29 | }) 30 | 31 | test('export last function as resultFunc', () => { 32 | const lastFunction = () => {} 33 | const selector = createSelector((state: StateA) => state.a, lastFunction) 34 | expect(selector.resultFunc).toBe(lastFunction) 35 | }) 36 | 37 | test('export dependencies as dependencies', () => { 38 | const dependency1 = (state: StateA) => { 39 | state.a 40 | } 41 | const dependency2 = (state: StateA) => { 42 | state.a 43 | } 44 | 45 | const selector = createSelector(dependency1, dependency2, () => {}) 46 | expect(selector.dependencies).toEqual([dependency1, dependency2]) 47 | }) 48 | 49 | test('export lastResult function', () => { 50 | const selector = createSelector( 51 | (state: StateAB) => state.a, 52 | (state: StateAB) => state.b, 53 | (a, b) => a + b 54 | ) 55 | 56 | const result = selector({ a: 1, b: 2 }) 57 | expect(result).toBe(3) 58 | expect(selector.lastResult()).toBe(3) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/setup.vitest.ts: -------------------------------------------------------------------------------- 1 | import { isMemoizedSelector } from './testUtils' 2 | 3 | expect.extend({ 4 | toBeMemoizedSelector(received) { 5 | const { isNot } = this 6 | 7 | return { 8 | pass: isMemoizedSelector(received), 9 | message: () => `${received} is${isNot ? '' : ' not'} a memoized selector` 10 | } 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/testTypes.ts: -------------------------------------------------------------------------------- 1 | export interface StateA { 2 | a: number 3 | } 4 | 5 | export interface StateAB { 6 | a: number 7 | b: number 8 | } 9 | 10 | export interface StateSub { 11 | sub: { 12 | a: number 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "emitDeclarationOnly": false, 9 | "strict": true, 10 | "noEmit": true, 11 | "target": "ESNext", 12 | "jsx": "react", 13 | "baseUrl": ".", 14 | "rootDir": "../", 15 | "skipLibCheck": true, 16 | "noImplicitReturns": false, 17 | "noUnusedLocals": false, 18 | "types": ["vitest/globals"], 19 | "paths": { 20 | "reselect": ["../src/index.ts"], // @remap-prod-remove-line 21 | "@internal/*": ["../src/*"] 22 | } 23 | }, 24 | "include": ["**/*.ts*"] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "allowJs": true, 10 | "jsx": "react", 11 | "noErrorTruncation": true, 12 | "declaration": true, 13 | "emitDeclarationOnly": true, 14 | "outDir": "dist", 15 | "forceConsistentCasingInFileNames": true, 16 | "experimentalDecorators": true, 17 | "rootDirs": ["./src"], 18 | "rootDir": ".", 19 | "types": ["vitest/globals", "vitest/importMeta"], 20 | "baseUrl": ".", 21 | "paths": { 22 | "reselect": ["src/index.ts"], // @remap-prod-remove-line 23 | "@internal/*": ["src/*"] 24 | } 25 | }, 26 | "include": ["./src/**/*"], 27 | "exclude": ["node_modules", "dist"] 28 | } 29 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import path from 'node:path' 3 | import type { Options } from 'tsup' 4 | import { defineConfig } from 'tsup' 5 | 6 | async function writeCommonJSEntry() { 7 | await fs.writeFile( 8 | path.join('dist/cjs/', 'index.js'), 9 | `'use strict' 10 | if (process.env.NODE_ENV === 'production') { 11 | module.exports = require('./reselect.production.min.cjs') 12 | } else { 13 | module.exports = require('./reselect.development.cjs') 14 | }` 15 | ) 16 | } 17 | 18 | export default defineConfig((options): Options[] => { 19 | const commonOptions: Options = { 20 | entry: { 21 | reselect: 'src/index.ts' 22 | }, 23 | sourcemap: true, 24 | target: ['esnext'], 25 | clean: true, 26 | ...options 27 | } 28 | 29 | return [ 30 | { 31 | ...commonOptions, 32 | name: 'Modern ESM', 33 | target: ['esnext'], 34 | format: ['esm'], 35 | outExtension: () => ({ js: '.mjs' }) 36 | }, 37 | 38 | // Support Webpack 4 by pointing `"module"` to a file with a `.js` extension 39 | // and optional chaining compiled away 40 | { 41 | ...commonOptions, 42 | name: 'Legacy ESM, Webpack 4', 43 | entry: { 44 | 'reselect.legacy-esm': 'src/index.ts' 45 | }, 46 | format: ['esm'], 47 | outExtension: () => ({ js: '.js' }), 48 | target: ['es2017'] 49 | }, 50 | 51 | // Meant to be served up via CDNs like `unpkg`. 52 | { 53 | ...commonOptions, 54 | name: 'Browser-ready ESM', 55 | entry: { 56 | 'reselect.browser': 'src/index.ts' 57 | }, 58 | platform: 'browser', 59 | env: { 60 | NODE_ENV: 'production' 61 | }, 62 | format: ['esm'], 63 | outExtension: () => ({ js: '.mjs' }), 64 | minify: true 65 | }, 66 | { 67 | ...commonOptions, 68 | name: 'CJS Development', 69 | entry: { 70 | 'reselect.development': 'src/index.ts' 71 | }, 72 | env: { 73 | NODE_ENV: 'development' 74 | }, 75 | format: ['cjs'], 76 | outDir: './dist/cjs/', 77 | outExtension: () => ({ js: '.cjs' }) 78 | }, 79 | { 80 | ...commonOptions, 81 | name: 'CJS production', 82 | entry: { 83 | 'reselect.production.min': 'src/index.ts' 84 | }, 85 | env: { 86 | NODE_ENV: 'production' 87 | }, 88 | format: ['cjs'], 89 | outDir: './dist/cjs/', 90 | outExtension: () => ({ js: '.cjs' }), 91 | minify: true, 92 | onSuccess: async () => { 93 | await writeCommonJSEntry() 94 | } 95 | }, 96 | { 97 | ...commonOptions, 98 | name: 'CJS Type Definitions', 99 | format: ['cjs'], 100 | dts: { only: true } 101 | } 102 | ] 103 | }) 104 | -------------------------------------------------------------------------------- /type-tests/createSelector.withTypes.test-d.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { describe, expectTypeOf, test } from 'vitest' 3 | 4 | interface Todo { 5 | id: number 6 | completed: boolean 7 | } 8 | 9 | interface Alert { 10 | id: number 11 | read: boolean 12 | } 13 | 14 | interface RootState { 15 | todos: Todo[] 16 | alerts: Alert[] 17 | } 18 | 19 | const rootState: RootState = { 20 | todos: [ 21 | { id: 0, completed: false }, 22 | { id: 1, completed: false } 23 | ], 24 | alerts: [ 25 | { id: 0, read: false }, 26 | { id: 1, read: false } 27 | ] 28 | } 29 | 30 | describe('createSelector.withTypes()', () => { 31 | const createAppSelector = createSelector.withTypes() 32 | 33 | describe('when input selectors are provided as a single array', () => { 34 | test('locks down state type and infers result function parameter types correctly', () => { 35 | expectTypeOf(createSelector.withTypes).returns.toEqualTypeOf( 36 | createSelector 37 | ) 38 | 39 | // Type of state is locked and the parameter types of the result function 40 | // are correctly inferred when input selectors are provided as a single array. 41 | createAppSelector( 42 | [ 43 | state => { 44 | expectTypeOf(state).toEqualTypeOf(rootState) 45 | 46 | return state.todos 47 | } 48 | ], 49 | todos => { 50 | expectTypeOf(todos).toEqualTypeOf(rootState.todos) 51 | 52 | return todos.map(({ id }) => id) 53 | } 54 | ) 55 | }) 56 | }) 57 | 58 | describe('when input selectors are provided as separate inline arguments', () => { 59 | test('locks down state type but does not infer result function parameter types', () => { 60 | // Type of state is locked but the parameter types of the 61 | // result function are NOT correctly inferred when 62 | // input selectors are provided as separate inline arguments. 63 | createAppSelector( 64 | state => { 65 | expectTypeOf(state).toEqualTypeOf(rootState) 66 | 67 | return state.todos 68 | }, 69 | todos => { 70 | // Known limitation: Parameter types are not inferred in this scenario 71 | expectTypeOf(todos).toBeAny() 72 | 73 | expectTypeOf(todos).not.toEqualTypeOf(rootState.todos) 74 | 75 | // @ts-expect-error A typed `createSelector` currently only infers 76 | // the parameter types of the result function when 77 | // input selectors are provided as a single array. 78 | return todos.map(({ id }) => id) 79 | } 80 | ) 81 | }) 82 | 83 | test('handles multiple input selectors with separate inline arguments', () => { 84 | // Checking to see if the type of state is correct when multiple 85 | // input selectors are provided as separate inline arguments. 86 | createAppSelector( 87 | state => { 88 | expectTypeOf(state).toEqualTypeOf(rootState) 89 | 90 | return state.todos 91 | }, 92 | state => { 93 | expectTypeOf(state).toEqualTypeOf(rootState) 94 | 95 | return state.alerts 96 | }, 97 | (todos, alerts) => { 98 | // Known limitation: Parameter types are not inferred in this scenario 99 | expectTypeOf(todos).toBeAny() 100 | 101 | expectTypeOf(alerts).toBeAny() 102 | 103 | // @ts-expect-error A typed `createSelector` currently only infers 104 | // the parameter types of the result function when 105 | // input selectors are provided as a single array. 106 | return todos.map(({ id }) => id) 107 | } 108 | ) 109 | }) 110 | 111 | test('can annotate parameter types of the result function to workaround type inference issue', () => { 112 | createAppSelector( 113 | state => state.todos, 114 | (todos: Todo[]) => todos.map(({ id }) => id) 115 | ) 116 | }) 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /type-tests/createSelectorCreator.test-d.ts: -------------------------------------------------------------------------------- 1 | import lodashMemoize from 'lodash/memoize' 2 | import memoizeOne from 'memoize-one' 3 | import microMemoize from 'micro-memoize' 4 | import { 5 | createSelectorCreator, 6 | lruMemoize, 7 | unstable_autotrackMemoize as autotrackMemoize, 8 | weakMapMemoize 9 | } from 'reselect' 10 | import { describe, test } from 'vitest' 11 | 12 | interface RootState { 13 | todos: { id: number; completed: boolean }[] 14 | alerts: { id: number; read: boolean }[] 15 | } 16 | 17 | const state: RootState = { 18 | todos: [ 19 | { id: 0, completed: false }, 20 | { id: 1, completed: true } 21 | ], 22 | alerts: [ 23 | { id: 0, read: false }, 24 | { id: 1, read: true } 25 | ] 26 | } 27 | 28 | describe('createSelectorCreator', () => { 29 | test('options object as argument', () => { 30 | const createSelectorDefault = createSelectorCreator({ 31 | memoize: lruMemoize 32 | }) 33 | const createSelectorWeakMap = createSelectorCreator({ 34 | memoize: weakMapMemoize 35 | }) 36 | const createSelectorAutotrack = createSelectorCreator({ 37 | memoize: autotrackMemoize 38 | }) 39 | const createSelectorMicro = createSelectorCreator({ 40 | memoize: microMemoize 41 | }) 42 | const createSelectorOne = createSelectorCreator({ 43 | memoize: memoizeOne 44 | }) 45 | const createSelectorLodash = createSelectorCreator({ 46 | memoize: lodashMemoize 47 | }) 48 | }) 49 | 50 | test('memoize function as argument', () => { 51 | const createSelectorDefault = createSelectorCreator(lruMemoize) 52 | const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) 53 | const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) 54 | const createSelectorMicro = createSelectorCreator(microMemoize) 55 | const createSelectorOne = createSelectorCreator(memoizeOne) 56 | const createSelectorLodash = createSelectorCreator(lodashMemoize) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /type-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "strict": true, 6 | "target": "ES2015", 7 | "lib": ["ES2021.WeakRef"], 8 | "declaration": true, 9 | "noEmit": true, 10 | "skipLibCheck": true, 11 | "paths": { 12 | "reselect": ["../src/index"], // @remap-prod-remove-line 13 | "@internal/*": ["../src/*"] 14 | } 15 | }, 16 | "include": ["**/*.ts", "../typescript_test/**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /typescript_test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "strict": true, 5 | "target": "ES2015", 6 | "lib": ["ES2021.WeakRef"], 7 | "declaration": true, 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "paths": { 11 | "reselect": ["../src/index"], // @remap-prod-remove-line 12 | "@internal/*": ["../src/*"] 13 | } 14 | }, 15 | "include": ["test.ts", "argsMemoize.typetest.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /typescript_test/typesTestUtils.ts: -------------------------------------------------------------------------------- 1 | export function expectType(t: T): T { 2 | return t 3 | } 4 | 5 | export declare type IsAny = true | false extends ( 6 | T extends never ? true : false 7 | ) 8 | ? True 9 | : False 10 | 11 | export declare type IsUnknown = unknown extends T 12 | ? IsAny 13 | : False 14 | 15 | type Equals = IsAny< 16 | T, 17 | never, 18 | IsAny 19 | > 20 | 21 | export type IsEqual = (() => G extends A ? 1 : 2) extends < 22 | G 23 | >() => G extends B ? 1 : 2 24 | ? true 25 | : false 26 | 27 | export function expectExactType(t: T) { 28 | return (u: U & Equals) => {} 29 | } 30 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | import path from 'node:path' 4 | import { fileURLToPath } from 'node:url' 5 | 6 | // No __dirname under Node ESM 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = path.dirname(__filename) 9 | 10 | export default defineConfig({ 11 | test: { 12 | typecheck: { tsconfig: 'type-tests/tsconfig.json' }, 13 | globals: true, 14 | include: ['./test/**/*.(spec|test).[jt]s?(x)'], 15 | setupFiles: ['test/setup.vitest.ts'], 16 | alias: { 17 | reselect: path.join(__dirname, 'src/index.ts'), // @remap-prod-remove-line 18 | 19 | // this mapping is disabled as we want `dist` imports in the tests only to be used for "type-only" imports which don't play a role for jest 20 | '@internal': path.join(__dirname, 'src') 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')] 3 | } 4 | -------------------------------------------------------------------------------- /website/docs/api/unstable_autotrackMemoize.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: unstable_autotrackMemoize 3 | title: unstable_autotrackMemoize 4 | sidebar_label: unstable_autotrackMemoize 5 | hide_title: true 6 | description: 'unstable_autotrackMemoize' 7 | --- 8 | 9 | import Tabs from '@theme/Tabs' 10 | import TabItem from '@theme/TabItem' 11 | import { InternalLinks } from '@site/src/components/InternalLinks' 12 | 13 | # `unstable_autotrackMemoize` 14 | 15 | Uses an "auto-tracking" approach inspired by the work of the Ember Glimmer team. It uses a Proxy to wrap arguments and track accesses to nested fields in your selector on first read. Later, when the selector is called with new arguments, it identifies which accessed fields have changed and only recalculates the result if one or more of those accessed fields have changed. This allows it to be more precise than the shallow equality checks in `lruMemoize`. 16 | 17 | :::danger 18 | 19 | This API is still experimental and undergoing testing. 20 | 21 | ::: 22 | 23 | ## Design Tradeoffs 24 | 25 | - Pros: 26 | 27 | - It is likely to avoid excess calculations and recalculate fewer times than `lruMemoize` will, which may also result in fewer component re-renders. 28 | 29 | - Cons: 30 | 31 | - It only has a cache size of 1. 32 | - It is slower than `lruMemoize`, because it has to do more work. (How much slower is dependent on the number of accessed fields in a selector, number of calls, frequency of input changes, etc) 33 | - It can have some unexpected behavior. Because it tracks nested field accesses, cases where you don't access a field will not recalculate properly. For example, a badly-written selector like: 34 | 35 | ```ts 36 | createSelector([state => state.todos], todos => todos) 37 | ``` 38 | 39 | that just immediately returns the extracted value will never update, because it doesn't see any field accesses to check. 40 | 41 | ## Use Cases 42 | 43 | - It is likely best used for cases where you need to access specific nested fields in data, and avoid recalculating if other fields in the same data objects are immutably updated. 44 | 45 | ## Parameters 46 | 47 | | Name | Description | 48 | | :----- | :--------------------------- | 49 | | `func` | The function to be memoized. | 50 | 51 | ## Returns 52 | 53 | A memoized function with a `.clearCache()` method attached. 54 | 55 | ## Type Parameters 56 | 57 | | Name | Description | 58 | | :----- | :----------------------------------------- | 59 | | `Func` | The type of the function that is memoized. | 60 | 61 | ## Examples 62 | 63 | ### Using `unstable_autotrackMemoize` with `createSelector` 64 | 65 | {/* START: unstable_autotrackMemoize/usingWithCreateSelector.ts */} 66 | 67 | 74 | 75 | 76 | ```ts title="unstable_autotrackMemoize/usingWithCreateSelector.ts" 77 | import { createSelector, unstable_autotrackMemoize } from 'reselect' 78 | 79 | export interface RootState { 80 | todos: { id: number; completed: boolean }[] 81 | alerts: { id: number; read: boolean }[] 82 | } 83 | 84 | const selectTodoIds = createSelector( 85 | [(state: RootState) => state.todos], 86 | todos => todos.map(todo => todo.id), 87 | { memoize: unstable_autotrackMemoize } 88 | ) 89 | ``` 90 | 91 | 92 | 93 | 94 | ```js title="unstable_autotrackMemoize/usingWithCreateSelector.js" 95 | import { createSelector, unstable_autotrackMemoize } from 'reselect' 96 | 97 | const selectTodoIds = createSelector( 98 | [state => state.todos], 99 | todos => todos.map(todo => todo.id), 100 | { memoize: unstable_autotrackMemoize } 101 | ) 102 | ``` 103 | 104 | 105 | 106 | 107 | {/* END: unstable_autotrackMemoize/usingWithCreateSelector.ts */} 108 | 109 | ### Using `unstable_autotrackMemoize` with `createSelectorCreator` 110 | 111 | {/* START: unstable_autotrackMemoize/usingWithCreateSelectorCreator.ts */} 112 | 113 | 120 | 121 | 122 | ```ts title="unstable_autotrackMemoize/usingWithCreateSelectorCreator.ts" 123 | import { createSelectorCreator, unstable_autotrackMemoize } from 'reselect' 124 | import type { RootState } from './usingWithCreateSelector' 125 | 126 | const createSelectorAutotrack = createSelectorCreator({ 127 | memoize: unstable_autotrackMemoize 128 | }) 129 | 130 | const selectTodoIds = createSelectorAutotrack( 131 | [(state: RootState) => state.todos], 132 | todos => todos.map(todo => todo.id) 133 | ) 134 | ``` 135 | 136 | 137 | 138 | 139 | ```js title="unstable_autotrackMemoize/usingWithCreateSelectorCreator.js" 140 | import { createSelectorCreator, unstable_autotrackMemoize } from 'reselect' 141 | 142 | const createSelectorAutotrack = createSelectorCreator({ 143 | memoize: unstable_autotrackMemoize 144 | }) 145 | 146 | const selectTodoIds = createSelectorAutotrack([state => state.todos], todos => 147 | todos.map(todo => todo.id) 148 | ) 149 | ``` 150 | 151 | 152 | 153 | 154 | {/* END: unstable_autotrackMemoize/usingWithCreateSelectorCreator.ts */} 155 | -------------------------------------------------------------------------------- /website/docs/external-references.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: external-references 3 | title: External References 4 | sidebar_label: External References 5 | hide_title: true 6 | description: External References 7 | --- 8 | 9 | import { AllExternalLinks } from '@site/src/components/ExternalLinks' 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/docs/introduction/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: getting-started 3 | title: Getting Started 4 | sidebar_label: Getting Started 5 | sidebar_position: 1 6 | hide_title: true 7 | description: 'Getting Started' 8 | --- 9 | 10 | import { InternalLinks } from '@site/src/components/InternalLinks' 11 | import { ExternalLinks } from '@site/src/components/ExternalLinks' 12 | import PackageTabs from '@site/src/components/PackageManagerTabs' 13 | import Tabs from '@theme/Tabs' 14 | import TabItem from '@theme/TabItem' 15 | import Link from '@docusaurus/Link' 16 | 17 | # Getting Started with Reselect 18 | 19 | A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well. 20 | 21 | - Selectors can compute derived data, allowing to store the minimal possible state. 22 | - Selectors are efficient. A selector is not recomputed unless one of its arguments changes. 23 | - Selectors are composable. They can be used as input to other selectors. 24 | 25 | The **Redux docs usage page on Deriving Data with Selectors** covers the purpose and motivation for selectors, why memoized selectors are useful, typical Reselect usage patterns, and using selectors with . 26 | 27 | ## Installation 28 | 29 | ### Redux Toolkit 30 | 31 | While Reselect is not exclusive to , it is already included by default in - no further installation needed. 32 | 33 | ```ts 34 | import { createSelector } from '@reduxjs/toolkit' 35 | ``` 36 | 37 | ### Standalone 38 | 39 | For standalone usage, install the `reselect` package: 40 | 41 | 42 | 43 | --- 44 | 45 | ## Basic Usage 46 | 47 | Reselect exports a `createSelector` API, which generates memoized selector functions. `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. 48 | 49 | You can play around with the following **example** in this CodeSandbox: 50 | 51 | {/* START: basicUsage.ts */} 52 | 53 | 60 | 61 | 62 | ```ts title="basicUsage.ts" 63 | import { createSelector } from 'reselect' 64 | 65 | interface RootState { 66 | todos: { id: number; completed: boolean }[] 67 | alerts: { id: number; read: boolean }[] 68 | } 69 | 70 | const state: RootState = { 71 | todos: [ 72 | { id: 0, completed: false }, 73 | { id: 1, completed: true } 74 | ], 75 | alerts: [ 76 | { id: 0, read: false }, 77 | { id: 1, read: true } 78 | ] 79 | } 80 | 81 | const selectCompletedTodos = (state: RootState) => { 82 | console.log('selector ran') 83 | return state.todos.filter(todo => todo.completed === true) 84 | } 85 | 86 | selectCompletedTodos(state) // selector ran 87 | selectCompletedTodos(state) // selector ran 88 | selectCompletedTodos(state) // selector ran 89 | 90 | const memoizedSelectCompletedTodos = createSelector( 91 | [(state: RootState) => state.todos], 92 | todos => { 93 | console.log('memoized selector ran') 94 | return todos.filter(todo => todo.completed === true) 95 | } 96 | ) 97 | 98 | memoizedSelectCompletedTodos(state) // memoized selector ran 99 | memoizedSelectCompletedTodos(state) 100 | memoizedSelectCompletedTodos(state) 101 | 102 | console.log(selectCompletedTodos(state) === selectCompletedTodos(state)) //=> false 103 | 104 | console.log( 105 | memoizedSelectCompletedTodos(state) === memoizedSelectCompletedTodos(state) 106 | ) //=> true 107 | ``` 108 | 109 | 110 | 111 | 112 | ```js title="basicUsage.js" 113 | import { createSelector } from 'reselect' 114 | 115 | const state = { 116 | todos: [ 117 | { id: 0, completed: false }, 118 | { id: 1, completed: true } 119 | ], 120 | alerts: [ 121 | { id: 0, read: false }, 122 | { id: 1, read: true } 123 | ] 124 | } 125 | 126 | const selectCompletedTodos = state => { 127 | console.log('selector ran') 128 | return state.todos.filter(todo => todo.completed === true) 129 | } 130 | 131 | selectCompletedTodos(state) // selector ran 132 | selectCompletedTodos(state) // selector ran 133 | selectCompletedTodos(state) // selector ran 134 | 135 | const memoizedSelectCompletedTodos = createSelector( 136 | [state => state.todos], 137 | todos => { 138 | console.log('memoized selector ran') 139 | return todos.filter(todo => todo.completed === true) 140 | } 141 | ) 142 | 143 | memoizedSelectCompletedTodos(state) // memoized selector ran 144 | memoizedSelectCompletedTodos(state) 145 | memoizedSelectCompletedTodos(state) 146 | 147 | console.log(selectCompletedTodos(state) === selectCompletedTodos(state)) //=> false 148 | 149 | console.log( 150 | memoizedSelectCompletedTodos(state) === memoizedSelectCompletedTodos(state) 151 | ) //=> true 152 | ``` 153 | 154 | 155 | 156 | 157 | {/* END: basicUsage.ts */} 158 | 159 | As you can see from the example above, `memoizedSelectCompletedTodos` does not run the second or third time, but we still get the same return value as last time. 160 | 161 | In addition to skipping unnecessary recalculations, `memoizedSelectCompletedTodos` returns the existing result reference if there is no recalculation. This is important for libraries like React-Redux or React that often rely on reference equality checks to optimize UI updates. 162 | 163 | --- 164 | 165 | ## Terminology 166 | 167 | - Selector Function} /> 168 | : A function that accepts one or more JavaScript values as arguments, and derives 169 | a result. When used with , the first argument is typically 170 | the entire Redux store state. 171 | - Input Selectors} /> 172 | : Basic selector functions used as building blocks for creating a memoized selector. 173 | They are passed as the first argument(s) to , and 174 | are called with all selector arguments. They are responsible for extracting and 175 | providing necessary values to the . 176 | - Output Selector} /> 177 | : The actual memoized selectors created by . 178 | - Result Function} /> 179 | : The function that comes after the 180 | . It takes the ' return values as arguments 181 | and returns a result. 182 | - Dependencies} /> 183 | : Same as 184 | . They are what the "depends" on. 185 | 186 | The below example serves as a visual aid: 187 | 188 | ```ts 189 | const outputSelector = createSelector( 190 | [inputSelector1, inputSelector2, inputSelector3], // synonymous with `dependencies`. 191 | resultFunc // Result function 192 | ) 193 | ``` 194 | -------------------------------------------------------------------------------- /website/docs/introduction/how-does-reselect-work.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: how-does-reselect-work 3 | title: How Does Reselect Work? 4 | sidebar_label: How Does Reselect Work? 5 | hide_title: true 6 | description: 'How Does Reselect Work?' 7 | --- 8 | 9 | import { InternalLinks } from '@site/src/components/InternalLinks' 10 | import { ExternalLinks } from '@site/src/components/ExternalLinks' 11 | 12 | # How Does Reselect Work? 13 | 14 | Reselect, at its core, is a library for creating memoized selectors in JavaScript applications. Its primary role is to efficiently compute derived data based on provided inputs. A key aspect of Reselect's internal mechanism is how it orchestrates the flow of arguments from the final selector to its constituent . 15 | 16 | ```ts 17 | const finalSelector = (...args) => { 18 | const extractedValues = inputSelectors.map(inputSelector => 19 | inputSelector(...args) 20 | ) 21 | return resultFunc(...extractedValues) 22 | } 23 | ``` 24 | 25 | In this pattern, the `finalSelector` is composed of several , **all receiving the same arguments as the final selector**. Each input selector processes its part of the data, and the results are then combined and further processed by the . Understanding this argument flow is crucial for appreciating how Reselect optimizes data computation and minimizes unnecessary recalculations. 26 | 27 | ## Cascading Memoization 28 | 29 | Reselect uses a two-stage "cascading" approach to memoizing functions: 30 | 31 | The way Reselect works can be broken down into multiple parts: 32 | 33 | 1. **Initial Run**: On the first call, Reselect runs all the , gathers their results, and passes them to the . 34 | 35 | 2. **Subsequent Runs**: For subsequent calls, Reselect performs two levels of checks: 36 | 37 | - **First Level**: It compares the current arguments with the previous ones (done by `argsMemoize`). 38 | 39 | - If they're the same, it returns the cached result without running the or the . 40 | 41 | - If they differ, it proceeds ("cascades") to the second level. 42 | 43 | - **Second Level**: It runs the and compares their current results with the previous ones (done by `memoize`). 44 | :::note 45 | 46 | If any one of the return a different result, all will recalculate. 47 | 48 | ::: 49 | 50 | - If the results are the same, it returns the cached result without running the . 51 | - If the results differ, it runs the . 52 | 53 | This behavior is what we call **_Cascading Memoization_**. 54 | 55 | ### Reselect Vs Standard Memoization 56 | 57 | #### Standard Memoization 58 | 59 | ![normal-memoization-function](@site/static/img/normal-memoization-function.png) 60 | 61 | _Standard memoization only compares arguments. If they're the same, it returns the cached result._ 62 | 63 | #### Memoization with Reselect 64 | 65 | ![reselect-memoization](@site/static/img/reselect-memoization.png) 66 | 67 | _Reselect adds a second layer of checks with the . This is crucial in applications where state references change frequently._ 68 | 69 | A normal function will compare the arguments, and if they are the same as last time, it will skip running the function and return the cached result. However, Reselect enhances this by introducing a second tier of checks via its . It's possible that the arguments passed to these may change, yet their results remain the same. When this occurs, Reselect avoids re-executing the , and returns the cached result. 70 | 71 | This feature becomes crucial in applications, where the `state` changes its reference anytime an `action` is dispatched. 72 | 73 | :::note 74 | 75 | The take the same arguments as the . 76 | 77 | ::: 78 | 79 | ## Why Reselect Is Often Used With Redux 80 | 81 | While Reselect can be used independently from Redux, it is a standard tool used in most Redux applications to help optimize calculations and UI updates: 82 | 83 | Imagine you have a selector like this: 84 | 85 | ```ts 86 | const selectCompletedTodos = (state: RootState) => 87 | state.todos.filter(todo => todo.completed === true) 88 | ``` 89 | 90 | So you decide to memoize it: 91 | 92 | ```ts 93 | const selectCompletedTodos = someMemoizeFunction((state: RootState) => 94 | state.todos.filter(todo => todo.completed === true) 95 | ) 96 | ``` 97 | 98 | Then you update `state.alerts`: 99 | 100 | ```ts 101 | store.dispatch(toggleRead(0)) 102 | ``` 103 | 104 | Now when you call `selectCompletedTodos`, it re-runs, because we have effectively broken memoization. 105 | 106 | ```ts 107 | selectCompletedTodos(store.getState()) 108 | // Will not run, and the cached result will be returned. 109 | selectCompletedTodos(store.getState()) 110 | store.dispatch(toggleRead(0)) 111 | // It recalculates. 112 | selectCompletedTodos(store.getState()) 113 | ``` 114 | 115 | But why? `selectCompletedTodos` only needs to access `state.todos`, and has nothing to do with `state.alerts`, so why have we broken memoization? Well that's because in anytime you make a change to the root `state`, it gets shallowly updated, which means its reference changes, therefore a normal memoization function will always fail the comparison check on the arguments. 116 | 117 | But with Reselect, we can do something like this: 118 | 119 | ```ts 120 | const selectCompletedTodos = createSelector( 121 | [(state: RootState) => state.todos], 122 | todos => todos.filter(todo => todo.completed === true) 123 | ) 124 | ``` 125 | 126 | And now we have achieved memoization: 127 | 128 | ```ts 129 | selectCompletedTodos(store.getState()) 130 | // Will not run, and the cached result will be returned. 131 | selectCompletedTodos(store.getState()) 132 | store.dispatch(toggleRead(0)) 133 | // The `input selectors` will run, but the `result function` is 134 | // skipped and the cached result will be returned. 135 | selectCompletedTodos(store.getState()) 136 | ``` 137 | 138 | Even when the overall `state` changes, Reselect ensures efficient memoization through its unique approach. The doesn't re-run if the relevant part of the `state` (in this case `state.todos`), remains unchanged. This is due to Reselect's . The first layer checks the entire `state`, and the second layer checks the results of the . If the first layer fails (due to a change in the overall `state`) but the second layer succeeds (because `state.todos` is unchanged), Reselect skips recalculating the . This dual-check mechanism makes Reselect particularly effective in applications, ensuring computations are only done when truly necessary. 139 | 140 | --- 141 | -------------------------------------------------------------------------------- /website/docs/introduction/v5-summary.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: v5-summary 3 | title: What's New in 5.0.0? 4 | sidebar_label: What's New in 5.0.0? 5 | hide_title: true 6 | description: What's New in 5.0.0? 7 | --- 8 | 9 | import { ExternalLinks } from '@site/src/components/ExternalLinks' 10 | import { InternalLinks } from '@site/src/components/InternalLinks' 11 | 12 | # What's New in 5.0.0? 13 | 14 | Version 5.0.0 introduces several new features and improvements: 15 | 16 | ## Customization Enhancements 17 | 18 | - Added the ability to pass an options object to , allowing for customized `memoize` and `argsMemoize` functions, alongside their respective options (`memoizeOptions` and `argsMemoizeOptions`). 19 | - The function now supports direct customization of `memoize` and `argsMemoize` within its options object. 20 | 21 | ## Memoization Functions 22 | 23 | - Introduced new experimental memoization functions: `weakMapMemoize` and `unstable_autotrackMemoize`. 24 | - Incorporated `memoize` and `argsMemoize` into the for debugging purposes. 25 | 26 | ## TypeScript Support and Performance 27 | 28 | - Discontinued support for TypeScript versions below 4.7, aligning with modern TypeScript features. 29 | - Significantly improved TypeScript performance for nesting . The nesting limit has increased from approximately 8 to around 30 , greatly reducing the occurrence of the infamous `Type instantiation is excessively deep and possibly infinite` error. 30 | 31 | ## Selector API Enhancements 32 | 33 | - Removed the second overload of due to its susceptibility to runtime errors. 34 | 35 | ## Additional Functionalities 36 | 37 | - Added `dependencyRecomputations` and `resetDependencyRecomputations` to the . These additions provide greater control and insight over , complementing the new `argsMemoize` API. 38 | - Introduced `inputStabilityCheck`, a development tool that runs the twice using the same arguments and triggers a warning If they return differing results for the same call. 39 | - Introduced `identityFunctionCheck`, a development tool that checks to see if the is an . 40 | 41 | These updates aim to enhance flexibility, performance, and developer experience. For detailed usage and examples, refer to the updated documentation sections for each feature. 42 | 43 | ## Breaking Changes 44 | 45 | - Switched the default memoization function used by `createSelector` to `weakMapMemoize`. 46 | - Renamed `defaultMemoize` to `lruMemoize` as it is no longer the default memoization function passed to `createSelector`. 47 | - Renamed `defaultEqualityCheck` to `referenceEqualityCheck`. 48 | - Renamed `DefaultMemoizeOptions` to `LruMemoizeOptions`. 49 | - Removed `ParametricSelector` and `OutputParametricSelector` types. Their functionalities are now integrated into `Selector` and `OutputSelector` respectively, which inherently support additional parameters. 50 | -------------------------------------------------------------------------------- /website/docs/related-projects.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: related-projects 3 | title: Related Projects 4 | sidebar_label: Related Projects 5 | hide_title: true 6 | description: Related Projects 7 | --- 8 | 9 | import { InternalLinks } from '@site/src/components/InternalLinks' 10 | import { ExternalLinks } from '@site/src/components/ExternalLinks' 11 | import Link from '@docusaurus/Link' 12 | 13 | # Related Projects 14 | 15 | ## Re-reselect 16 | 17 | Enhances Reselect selectors by wrapping and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function. 18 | 19 | Useful to reduce selectors recalculation when the same selector is repeatedly called with one/few different arguments. 20 | 21 | ## reselect-tools 22 | 23 | - Measure selector recomputations across the app and identify performance bottlenecks 24 | - Check selector dependencies, inputs, outputs, and recomputations at any time with the chrome extension 25 | - Statically export a JSON representation of your selector graph for further analysis 26 | 27 | ## reselect-debugger 28 | 29 | 30 | Flipper plugin and 31 | the connect app for debugging selectors in **React Native Apps**. 32 | 33 | Inspired by Reselect Tools, so it also has all functionality from this library and more, but only for React Native and Flipper. 34 | 35 | - Selectors Recomputations count in live time across the App for identify performance bottlenecks 36 | - Highlight most recomputed selectors 37 | - Dependency Graph 38 | - Search by Selectors Graph 39 | - Selectors Inputs 40 | - Selectors Output (In case if selector not dependent from external arguments) 41 | - Shows "Not Memoized (NM)" selectors 42 | -------------------------------------------------------------------------------- /website/docs/usage/best-practices.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: best-practices 3 | title: Best Practices 4 | sidebar_label: Best Practices 5 | hide_title: true 6 | description: Best Practices 7 | --- 8 | 9 | import { InternalLinks } from '@site/src/components/InternalLinks' 10 | import { ExternalLinks } from '@site/src/components/ExternalLinks' 11 | 12 | # Best Practices 13 | 14 | There are a few details that will help you skip running as many functions as possible and get the best possible performance out of Reselect: 15 | 16 | - Due to the in Reselect, The first layer of checks is upon the arguments that are passed to the , therefore it's best to maintain the same reference for the arguments as much as possible. 17 | - In , your state will change reference when updated. But it's best to keep the additional arguments as simple as possible, you can pass in objects or array as long as their reference does not change. Or you can pass in primitives like numbers for ids. 18 | - Keep your as simple as possible. It's best if they mostly consist of field accessors like `state => state.todos` or argument providers like `(state, id) => id`. You should not be doing any sort of calculation inside , and you should definitely not be returning an object or array with a new reference each time. 19 | - The is only re-run as a last resort. So make sure to put any and all calculations inside your . That way, Reselect will only run those calculations if all other checks fail. 20 | 21 | This: 22 | 23 | ```ts 24 | // ✔️ This is optimal because we have less calculations in input selectors and more in the result function. 25 | const selectorGood = createSelector( 26 | [(state: RootState) => state.todos], 27 | todos => someExpensiveComputation(todos) 28 | ) 29 | ``` 30 | 31 | Is preferable to this: 32 | 33 | ```ts 34 | // ❌ This is not optimal! 35 | const selectorBad = createSelector( 36 | [(state: RootState) => someExpensiveComputation(state.todos)], 37 | someOtherCalculation 38 | ) 39 | ``` 40 | -------------------------------------------------------------------------------- /website/docs/usage/common-mistakes.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: common-mistakes 3 | title: Common Mistakes 4 | sidebar_label: Common Mistakes 5 | hide_title: true 6 | description: Common Mistakes 7 | --- 8 | 9 | import { InternalLinks } from '@site/src/components/InternalLinks' 10 | 11 | # Common Mistakes 12 | 13 | A somewhat common mistake is to write an that extracts a value or does some derivation, and a that just returns its result: 14 | 15 | ```ts 16 | // ❌ BROKEN: this will not memoize correctly, and does nothing useful! 17 | const brokenSelector = createSelector( 18 | [(state: RootState) => state.todos], 19 | todos => todos 20 | ) 21 | ``` 22 | 23 | Any that just returns its inputs is incorrect! The should always have the transformation logic. 24 | 25 | Similarly: 26 | 27 | ```ts 28 | // ❌ BROKEN: this will not memoize correctly! 29 | const brokenSelector = createSelector( 30 | [(state: RootState) => state], 31 | state => state.todos 32 | ) 33 | ``` 34 | -------------------------------------------------------------------------------- /website/docs/usage/handling-empty-array-results.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: handling-empty-array-results 3 | title: Handling Empty Array Results 4 | sidebar_label: Handling Empty Array Results 5 | hide_title: true 6 | description: Handling Empty Array Results 7 | --- 8 | 9 | import Tabs from '@theme/Tabs' 10 | import TabItem from '@theme/TabItem' 11 | import { InternalLinks } from '@site/src/components/InternalLinks' 12 | 13 | # Handling Empty Array Results 14 | 15 | To reduce recalculations, use a predefined empty array when `array.filter` or similar methods result in an empty array. 16 | 17 | So you can have a pattern like this: 18 | 19 | {/* START: handling-empty-array-results/firstPattern.ts */} 20 | 21 | 28 | 29 | 30 | ```ts title="handling-empty-array-results/firstPattern.ts" 31 | import { createSelector } from 'reselect' 32 | 33 | export interface RootState { 34 | todos: { 35 | id: number 36 | title: string 37 | description: string 38 | completed: boolean 39 | }[] 40 | } 41 | 42 | const EMPTY_ARRAY: [] = [] 43 | 44 | const selectCompletedTodos = createSelector( 45 | [(state: RootState) => state.todos], 46 | todos => { 47 | const completedTodos = todos.filter(todo => todo.completed === true) 48 | return completedTodos.length === 0 ? EMPTY_ARRAY : completedTodos 49 | } 50 | ) 51 | ``` 52 | 53 | 54 | 55 | 56 | ```js title="handling-empty-array-results/firstPattern.js" 57 | import { createSelector } from 'reselect' 58 | 59 | const EMPTY_ARRAY = [] 60 | 61 | const selectCompletedTodos = createSelector([state => state.todos], todos => { 62 | const completedTodos = todos.filter(todo => todo.completed === true) 63 | return completedTodos.length === 0 ? EMPTY_ARRAY : completedTodos 64 | }) 65 | ``` 66 | 67 | 68 | 69 | 70 | {/* END: handling-empty-array-results/firstPattern.ts */} 71 | 72 | Or to avoid repetition, you can create a wrapper function and reuse it: 73 | 74 | {/* START: handling-empty-array-results/fallbackToEmptyArray.ts */} 75 | 76 | 83 | 84 | 85 | ```ts title="handling-empty-array-results/fallbackToEmptyArray.ts" 86 | import { createSelector } from 'reselect' 87 | import type { RootState } from './firstPattern' 88 | 89 | const EMPTY_ARRAY: [] = [] 90 | 91 | export const fallbackToEmptyArray = (array: T[]) => { 92 | return array.length === 0 ? EMPTY_ARRAY : array 93 | } 94 | 95 | const selectCompletedTodos = createSelector( 96 | [(state: RootState) => state.todos], 97 | todos => { 98 | return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) 99 | } 100 | ) 101 | ``` 102 | 103 | 104 | 105 | 106 | ```js title="handling-empty-array-results/fallbackToEmptyArray.js" 107 | import { createSelector } from 'reselect' 108 | 109 | const EMPTY_ARRAY = [] 110 | 111 | export const fallbackToEmptyArray = array => { 112 | return array.length === 0 ? EMPTY_ARRAY : array 113 | } 114 | 115 | const selectCompletedTodos = createSelector([state => state.todos], todos => { 116 | return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) 117 | }) 118 | ``` 119 | 120 | 121 | 122 | 123 | {/* END: handling-empty-array-results/fallbackToEmptyArray.ts */} 124 | 125 | This way if the returns an empty array twice in a row, your component will not re-render due to a stable empty array reference: 126 | 127 | ```ts 128 | const completedTodos = selectCompletedTodos(store.getState()) 129 | 130 | store.dispatch(addTodo()) 131 | 132 | console.log(completedTodos === selectCompletedTodos(store.getState())) //=> true 133 | ``` 134 | -------------------------------------------------------------------------------- /website/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options, ThemeConfig } from '@docusaurus/preset-classic' 2 | import type { Config } from '@docusaurus/types' 3 | 4 | const config: Config = { 5 | title: 'Reselect', 6 | tagline: 'A memoized selector library for Redux', 7 | favicon: 'img/favicon.ico', 8 | 9 | // Set the production url of your site here 10 | url: 'https://reselect.js.org', 11 | // Set the // pathname under which your site is served 12 | // For GitHub pages deployment, it is often '//' 13 | baseUrl: '/', 14 | 15 | // GitHub pages deployment config. 16 | // If you aren't using GitHub pages, you don't need these. 17 | organizationName: 'reduxjs', // Usually your GitHub org/user name. 18 | projectName: 'reselect', // Usually your repo name. 19 | 20 | onBrokenLinks: 'throw', 21 | onBrokenMarkdownLinks: 'warn', 22 | 23 | // Even if you don't use internationalization, you can use this field to set 24 | // useful metadata like html lang. For example, if your site is Chinese, you 25 | // may want to replace "en" with "zh-Hans". 26 | i18n: { 27 | defaultLocale: 'en', 28 | locales: ['en'] 29 | }, 30 | 31 | presets: [ 32 | [ 33 | 'classic', 34 | { 35 | docs: { 36 | path: 'docs', 37 | sidebarPath: './sidebars.ts', 38 | showLastUpdateTime: true, 39 | routeBasePath: '/', 40 | // Please change this to your repo. 41 | // Remove this to remove the "edit this page" links. 42 | editUrl: 'https://github.com/reduxjs/reselect/edit/master/website' 43 | }, 44 | theme: { 45 | customCss: './src/css/custom.css' 46 | } 47 | } satisfies Options 48 | ] 49 | ], 50 | 51 | themeConfig: { 52 | // Replace with your project's social card 53 | // image: 'img/docusaurus-social-card.jpg', 54 | navbar: { 55 | title: 'Reselect', 56 | 57 | items: [ 58 | { 59 | type: 'doc', 60 | position: 'right', 61 | label: 'Getting Started', 62 | docId: 'introduction/getting-started' 63 | }, 64 | { 65 | type: 'doc', 66 | position: 'right', 67 | label: 'API', 68 | docId: 'api/createSelector' 69 | }, 70 | { 71 | href: 'https://www.github.com/reduxjs/reselect', 72 | label: 'GitHub', 73 | position: 'right' 74 | } 75 | ] 76 | }, 77 | footer: { 78 | style: 'dark', 79 | links: [ 80 | { 81 | title: 'Community', 82 | items: [ 83 | { 84 | label: 'Stack Overflow', 85 | href: 'https://stackoverflow.com/questions/tagged/reselect' 86 | }, 87 | { 88 | label: 'Discord', 89 | href: 'https://discord.gg/0ZcbPKXt5bZ6au5t' 90 | } 91 | ] 92 | }, 93 | { 94 | title: 'More', 95 | items: [ 96 | { 97 | label: 'GitHub', 98 | href: 'https://github.com/reduxjs/reselect' 99 | } 100 | ] 101 | } 102 | ], 103 | copyright: `Copyright © ${new Date().getFullYear()} by the Redux Maintainers. Built with Docusaurus.` 104 | }, 105 | prism: { 106 | theme: require('./monokaiTheme.js') 107 | } 108 | } satisfies ThemeConfig 109 | } 110 | 111 | export default config 112 | -------------------------------------------------------------------------------- /website/insertCodeExamples.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, readdirSync, writeFileSync } from 'node:fs' 2 | import path from 'node:path' 3 | import { 4 | EXAMPLES_DIRECTORY, 5 | getTSConfig, 6 | hasTSXExtension, 7 | tsExtensionRegex 8 | } from './compileExamples' 9 | 10 | const placeholderRegex = 11 | /\{\/\* START: (.*?) \*\/\}([\s\S]*?)\{\/\* END: \1 \*\/\}/g 12 | 13 | const collectMarkdownFiles = ( 14 | directory: string, 15 | files: { path: string; content: string }[] = [] 16 | ) => { 17 | readdirSync(directory, { 18 | withFileTypes: true 19 | }).forEach(entry => { 20 | const filePath = path.join(directory, entry.name) 21 | if (entry.isDirectory()) { 22 | collectMarkdownFiles(filePath, files) 23 | } else if (/\.mdx?$/.test(entry.name)) { 24 | const content = readFileSync(filePath, 'utf-8') 25 | if (content.match(placeholderRegex)) { 26 | files.push({ path: filePath, content }) 27 | } 28 | } 29 | }) 30 | return files 31 | } 32 | 33 | const insertCodeExamples = (examplesDirectory: string) => { 34 | const frontMatterRegex = /---\s*[\s\S]*?---/ 35 | const markdownFilesPaths = collectMarkdownFiles('docs') 36 | markdownFilesPaths.forEach(({ path: markdownFilePath, content }) => { 37 | const importTabs = content.includes('import Tabs from') 38 | ? '' 39 | : `import Tabs from '@theme/Tabs'\n` 40 | const importTabItem = content.includes('import TabItem from') 41 | ? '' 42 | : `import TabItem from '@theme/TabItem'\n` 43 | content = content.replace( 44 | frontMatterRegex, 45 | frontMatter => `${frontMatter}\n${importTabs}${importTabItem}` 46 | ) 47 | 48 | content = content.replace( 49 | placeholderRegex, 50 | (placeholder, tsFileName: string) => { 51 | const isTSX = hasTSXExtension(tsFileName) 52 | 53 | const tsFileExtension = isTSX ? 'tsx' : 'ts' 54 | const jsFileExtension = isTSX ? 'jsx' : 'js' 55 | 56 | const jsFileName = tsFileName.replace( 57 | tsExtensionRegex, 58 | `.${jsFileExtension}` 59 | ) 60 | 61 | const tsFilePath = path.join(examplesDirectory, tsFileName) 62 | const jsFilePath = path.join( 63 | examplesDirectory, 64 | getTSConfig(examplesDirectory).compilerOptions.outDir, 65 | tsFileName.replace(tsExtensionRegex, `.${jsFileExtension}`) 66 | ) 67 | 68 | const tsFileContent = readFileSync(tsFilePath, 'utf-8') 69 | const jsFileContent = readFileSync(jsFilePath, 'utf-8') 70 | 71 | return `{/* START: ${tsFileName} */} 72 | 73 | 80 | 81 | 82 | \`\`\`${tsFileExtension} title="${tsFileName}" 83 | ${tsFileContent} 84 | \`\`\` 85 | 86 | 87 | 88 | \`\`\`${jsFileExtension} title="${jsFileName}" 89 | ${jsFileContent} 90 | \`\`\` 91 | 92 | 93 | 94 | {/* END: ${tsFileName} */}` 95 | } 96 | ) 97 | writeFileSync(markdownFilePath, content) 98 | }) 99 | } 100 | 101 | insertCodeExamples(EXAMPLES_DIRECTORY) 102 | -------------------------------------------------------------------------------- /website/monokaiTheme.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plain: { 3 | color: '#f8f8f2', 4 | backgroundColor: '#272822' 5 | }, 6 | styles: [ 7 | { 8 | types: ['comment', 'prolog', 'doctype', 'cdata'], 9 | style: { 10 | color: '#778090' 11 | } 12 | }, 13 | { 14 | types: ['punctuation'], 15 | style: { 16 | color: '#F8F8F2' 17 | } 18 | }, 19 | { 20 | types: ['property', 'tag', 'constant', 'symbol', 'deleted'], 21 | style: { 22 | color: '#F92672' 23 | } 24 | }, 25 | { 26 | types: ['boolean', 'number'], 27 | style: { 28 | color: '#AE81FF' 29 | } 30 | }, 31 | { 32 | types: ['selector', 'attr-name', 'string', 'char', 'builtin', 'inserted'], 33 | style: { 34 | color: '#a6e22e' 35 | } 36 | }, 37 | { 38 | types: ['operator', 'entity', 'url', 'variable'], 39 | style: { 40 | color: '#F8F8F2' 41 | } 42 | }, 43 | { 44 | types: ['atrule', 'attr-value', 'function'], 45 | style: { 46 | color: '#E6D874' 47 | } 48 | }, 49 | { 50 | types: ['keyword'], 51 | style: { 52 | color: '#F92672' 53 | } 54 | }, 55 | { 56 | types: ['regex', 'important'], 57 | style: { 58 | color: '#FD971F' 59 | } 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "scripts": { 4 | "docusaurus": "docusaurus", 5 | "start": "yarn prestart && docusaurus start", 6 | "build": "docusaurus build", 7 | "swizzle": "docusaurus swizzle", 8 | "deploy": "docusaurus deploy", 9 | "clear": "docusaurus clear", 10 | "serve": "docusaurus serve", 11 | "write-translations": "docusaurus write-translations", 12 | "write-heading-ids": "docusaurus write-heading-ids", 13 | "prestart": "yarn examples:clean && yarn examples:build && ts-node insertCodeExamples.ts && yarn format", 14 | "format": "prettier --write \"**/*.{ts,tsx}\" \"docs\"", 15 | "examples:clean": "rimraf ../docs/examples/dist", 16 | "examples:format": "prettier --write ../docs/examples", 17 | "examples:build": "ts-node compileExamples.ts && yarn examples:format", 18 | "typecheck": "tsc" 19 | }, 20 | "dependencies": { 21 | "@docusaurus/core": "3.0.0", 22 | "@docusaurus/preset-classic": "3.0.0", 23 | "@mdx-js/react": "^3.0.0", 24 | "clsx": "^1.2.1", 25 | "prism-react-renderer": "^2.1.0", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0" 28 | }, 29 | "devDependencies": { 30 | "@docusaurus/module-type-aliases": "3.0.0", 31 | "@docusaurus/tsconfig": "3.0.0", 32 | "@docusaurus/types": "3.0.0", 33 | "netlify-plugin-cache": "^1.0.3", 34 | "prettier": "^3.1.0", 35 | "rimraf": "^5.0.5", 36 | "ts-node": "^10.9.1", 37 | "typescript": "~5.2.2" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.5%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 3 chrome version", 47 | "last 3 firefox version", 48 | "last 5 safari version" 49 | ] 50 | }, 51 | "engines": { 52 | "node": ">=18.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /website/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from '@docusaurus/plugin-content-docs' 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | docsSidebar: [ 16 | // { type: 'autogenerated', dirName: '.' } 17 | { 18 | type: 'category', 19 | collapsed: false, 20 | label: 'Introduction', 21 | items: [ 22 | 'introduction/getting-started', 23 | 'introduction/how-does-reselect-work', 24 | 'introduction/v5-summary' 25 | ] 26 | }, 27 | { 28 | type: 'category', 29 | collapsed: false, 30 | label: 'API', 31 | items: [ 32 | 'api/createSelector', 33 | 'api/createSelectorCreator', 34 | 'api/createStructuredSelector', 35 | 'api/development-only-stability-checks', 36 | { 37 | type: 'category', 38 | collapsed: false, 39 | label: 'Memoization Functions', 40 | items: [ 41 | 'api/lruMemoize', 42 | 'api/weakMapMemoize', 43 | 'api/unstable_autotrackMemoize' 44 | ] 45 | } 46 | ] 47 | }, 48 | 49 | { 50 | type: 'category', 51 | label: 'Using Reselect', 52 | items: [ 53 | 'usage/best-practices', 54 | 'usage/common-mistakes', 55 | 'usage/handling-empty-array-results' 56 | ] 57 | }, 58 | 'FAQ', 59 | 'external-references', 60 | 'related-projects' 61 | ] 62 | } 63 | 64 | export default sidebars 65 | -------------------------------------------------------------------------------- /website/src/components/ExternalLinks.tsx: -------------------------------------------------------------------------------- 1 | import Link from '@docusaurus/Link' 2 | import type { FC, ReactNode } from 'react' 3 | import { memo } from 'react' 4 | 5 | interface Props { 6 | readonly text?: ReactNode 7 | } 8 | 9 | export const ExternalLinks = { 10 | WeakMap: memo(({ text = 'WeakMap' }) => ( 11 | 15 | {text} 16 | 17 | )), 18 | ReferenceEqualityCheck: memo(({ text = 'Reference Equality Check' }) => ( 19 | 23 | {text} 24 | 25 | )), 26 | Memoization: memo(({ text = 'memoization' }) => ( 27 | 28 | {text} 29 | 30 | )), 31 | IdentityFunction: memo(({ text = 'Identity Function' }) => ( 32 | 36 | {text} 37 | 38 | )), 39 | UseMemo: memo(({ text = 'useMemo' }) => ( 40 | 44 | {text} 45 | 46 | )), 47 | ReReselect: memo(({ text = 'Re-reselect' }) => ( 48 | 49 | {text} 50 | 51 | )), 52 | Redux: memo(({ text = 'Redux' }) => ( 53 | 54 | {text} 55 | 56 | )), 57 | React: memo(({ text = 'React' }) => ( 58 | 59 | {text} 60 | 61 | )), 62 | ReactRedux: memo(({ text = 'React-Redux' }) => ( 63 | 64 | {text} 65 | 66 | )), 67 | ReduxToolkit: memo(({ text = 'Redux-Toolkit' }) => ( 68 | 69 | {text} 70 | 71 | )) 72 | } as const satisfies Record> 73 | 74 | export const AllExternalLinks: FC = memo(() => { 75 | return ( 76 |
    77 | {Object.values(ExternalLinks).map((ExternalLink, index) => ( 78 |
  • 79 | 80 | 81 | 82 |
  • 83 | ))} 84 |
85 | ) 86 | }) 87 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import Heading from '@theme/Heading' 2 | import clsx from 'clsx' 3 | import type { FC, JSX } from 'react' 4 | import { memo } from 'react' 5 | import styles from './styles.module.css' 6 | 7 | interface FeatureItem { 8 | title: string 9 | description: JSX.Element 10 | } 11 | 12 | const FeatureList: FeatureItem[] = [ 13 | { 14 | title: 'Predictable', 15 | description: ( 16 | <> 17 | Like Redux, Reselect gives users a consistent mental model for 18 | memoizing functions. Extract input values, recalculate when any input 19 | changes. 20 | 21 | ) 22 | }, 23 | { 24 | title: 'Optimized', 25 | description: ( 26 | <> 27 | Reselect{' '} 28 | 29 | minimizes the number of times expensive computations are performed 30 | 31 | , reuses existing result references if nothing has changed, and improves 32 | performance. 33 | 34 | ) 35 | }, 36 | { 37 | title: 'Customizable', 38 | description: ( 39 | <> 40 | Reselect comes with fast defaults, but provides{' '} 41 | flexible customization options. Swap memoization methods, change 42 | equality checks, and customize for your needs. 43 | 44 | ) 45 | }, 46 | { 47 | title: 'Type-Safe', 48 | description: ( 49 | <> 50 | Reselect is designed for great TypeScript support. Generated 51 | selectors infer all types from input selectors. 52 | 53 | ) 54 | } 55 | ] 56 | 57 | const Feature: FC = memo(({ title, description }) => { 58 | return ( 59 |
60 |
61 | {title} 62 |

{description}

63 |
64 |
65 | ) 66 | }) 67 | 68 | const HomepageFeatures: FC = () => { 69 | return ( 70 |
71 |
72 |
73 | {FeatureList.map((props, idx) => ( 74 | 75 | ))} 76 |
77 |
78 |
79 | ) 80 | } 81 | 82 | export default memo(HomepageFeatures) 83 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /website/src/components/InternalLinks.tsx: -------------------------------------------------------------------------------- 1 | import Link from '@docusaurus/Link' 2 | import type { FC, ReactNode } from 'react' 3 | import { memo } from 'react' 4 | 5 | interface Props { 6 | readonly text: ReactNode 7 | } 8 | 9 | export const InternalLinks = { 10 | Selector: memo(({ text = 'selector' }) => ( 11 | 15 | {text} 16 | 17 | )), 18 | InputSelectors: memo(({ text = 'input selectors' }) => ( 19 | 23 | {text} 24 | 25 | )), 26 | OutputSelector: memo(({ text = 'output selector' }) => ( 27 | 31 | {text} 32 | 33 | )), 34 | ResultFunction: memo(({ text = 'result function' }) => ( 35 | 39 | {text} 40 | 41 | )), 42 | Dependencies: memo(({ text = 'dependencies' }) => ( 43 | 44 | {text} 45 | 46 | )), 47 | CascadingMemoization: memo(({ text = 'Cascading Memoization' }) => ( 48 | 52 | "{text}" 53 | 54 | )), 55 | OutputSelectorFields: memo(({ text = 'Output Selector Fields' }) => ( 56 | 60 | {text} 61 | 62 | )), 63 | CreateSelector: memo(() => ( 64 | 65 | createSelector 66 | 67 | )), 68 | CreateSelectorCreator: memo(() => ( 69 | 70 | createSelectorCreator 71 | 72 | )), 73 | LruMemoize: memo(() => ( 74 | 75 | lruMemoize 76 | 77 | )), 78 | WeakMapMemoize: memo(() => ( 79 | 80 | weakMapMemoize 81 | 82 | )), 83 | UnstableAutotrackMemoize: memo(() => ( 84 | 85 | unstable_autotrackMemoize 86 | 87 | )), 88 | CreateStructuredSelector: memo(() => ( 89 | 90 | createStructuredSelector 91 | 92 | )) 93 | } as const satisfies Record> 94 | -------------------------------------------------------------------------------- /website/src/components/PackageManagerTabs.tsx: -------------------------------------------------------------------------------- 1 | import CodeBlock from '@theme/CodeBlock' 2 | import TabItem from '@theme/TabItem' 3 | import Tabs from '@theme/Tabs' 4 | import type { FC } from 'react' 5 | import { memo } from 'react' 6 | 7 | const PACKAGE_NAME = 'reselect' 8 | 9 | const packageManagers = [ 10 | { value: 'npm', label: 'NPM', command: 'install' }, 11 | { value: 'yarn', label: 'Yarn', command: 'add' }, 12 | { value: 'bun', label: 'Bun', command: 'add' }, 13 | { value: 'pnpm', label: 'PNPM', command: 'add' } 14 | ] as const 15 | 16 | const PackageManagerTabs: FC = () => { 17 | return ( 18 | 19 | {packageManagers.map(({ value, command, label }) => ( 20 | 21 | 22 | {value} {command} {PACKAGE_NAME} 23 | 24 | 25 | ))} 26 | 27 | ) 28 | } 29 | 30 | export default memo(PackageManagerTabs) 31 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #764abc; 10 | --ifm-color-primary-dark: #6a43a9; 11 | --ifm-color-primary-darker: #5e3b96; 12 | --ifm-color-primary-darkest: #533484; 13 | --ifm-color-primary-light: #845cc3; 14 | --ifm-color-primary-lighter: #916ec9; 15 | --ifm-color-primary-lightest: #9f80d0; 16 | --ifm-code-font-size: 95%; 17 | --ifm-code-border-radius: 3px; 18 | --ifm-code-background: rgba(27, 31, 35, 0.05); 19 | 20 | --ifm-blockquote-color: #ecf4f9; 21 | --ifm-blockquote-color-dark: #cbddea; 22 | --blockquote-text-color: var(--ifm-font-base-color); 23 | 24 | --ifm-code-padding-vertical: 0.1rem; 25 | --ifm-code-padding-horizontal: 0.2rem; 26 | 27 | --ifm-tabs-padding-vertical: 0.2rem; 28 | --ifm-tabs-padding-horizontal: 0.4rem; 29 | 30 | --ifm-pre-background: rgb(39, 40, 34); 31 | --ifm-alert-color: black; 32 | 33 | --ifm-menu-color-active: var(--ifm-blockquote-color); 34 | 35 | --ra-admonition-color: #ecf4f9; 36 | --ra-admonition-color-dark: #2a98b9; 37 | 38 | --ra-admonition-color-important: #2a98b9; 39 | 40 | --ra-admonition-color-success: #f1fdf9; 41 | --ra-admonition-color-success-dark: #00bf88; 42 | 43 | --ra-admonition-color-caution: #fffbf5; 44 | --ra-admonition-color-caution-dark: #f0ad4e; 45 | 46 | --ra-admonition-color-error: #fff2f2; 47 | --ra-admonition-color-error-dark: #d9534f; 48 | 49 | --ra-admonition-icon-color: black !important; 50 | 51 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 52 | } 53 | 54 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 55 | [data-theme='dark'] { 56 | --ifm-color-primary: #ba8fff; 57 | --ifm-color-primary-dark: #7431ca; 58 | --ifm-color-primary-darker: #6d1cac; 59 | --ifm-color-primary-darkest: #730c9a; 60 | --ifm-color-primary-light: #b97cfd; 61 | --ifm-color-primary-lighter: #cc8ffc; 62 | --ifm-color-primary-lightest: #fcf2ff; 63 | --ifm-blockquote-color: #ecf4f9; 64 | --ifm-blockquote-color-dark: #6d1cac; 65 | --ifm-menu-color-active: black; 66 | --blockquote-text-color: black; 67 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 68 | } 69 | 70 | :root[data-theme='dark'] .hero.hero--primary { 71 | --ifm-hero-background-color: #593d88; 72 | --ifm-hero-text-color: #ffffff; 73 | } 74 | 75 | .admonition a, 76 | blockquote a { 77 | color: var(--ifm-color-primary-darkest); 78 | text-decoration: none; 79 | } 80 | .admonition a:hover, 81 | blockquote a:hover { 82 | color: var(--blockquote-text-color); 83 | } 84 | 85 | blockquote { 86 | color: var(--blockquote-text-color); 87 | background-color: var(--ifm-blockquote-color); 88 | border-left: 6px solid var(--ifm-blockquote-color-dark); 89 | border-radius: var(--ifm-global-radius); 90 | } 91 | 92 | .docusaurus-highlight-code-line { 93 | background-color: rgb(72, 77, 91); 94 | display: block; 95 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 96 | padding-top: 0; 97 | padding-bottom: 0; 98 | padding-left: calc(-0.25em + var(--ifm-pre-padding)); 99 | padding-right: var(--ifm-pre-padding); 100 | border-left: 0.25em solid #1976d2; 101 | } 102 | div[class*='codeBlockTitle'] { 103 | padding: 0.15rem var(--ifm-pre-padding); 104 | } 105 | 106 | :root[data-theme='dark'] .admonition code { 107 | color: var(--ifm-blockquote-color); 108 | } 109 | 110 | :root[data-theme='dark'] blockquote code { 111 | color: var(--ifm-blockquote-color); 112 | } 113 | 114 | code { 115 | background-color: var(--ifm-color-emphasis-300); 116 | border-radius: 0.2rem; 117 | } 118 | 119 | a code, 120 | code a { 121 | background-color: var(--ifm-color-emphasis-200); 122 | color: inherit; 123 | } 124 | 125 | a.contents__link > code { 126 | color: inherit; 127 | } 128 | 129 | a.contents__link.contents__link--active { 130 | font-weight: 600; 131 | } 132 | 133 | a:visited { 134 | color: var(--ifm-color-primary); 135 | } 136 | .navbar .navbar__inner { 137 | flex-wrap: nowrap; 138 | } 139 | .navbar .navbar__items { 140 | flex: 1 1 auto; 141 | } 142 | .footer__logo { 143 | width: 50px; 144 | height: 50px; 145 | } 146 | 147 | .menu__link { 148 | font-weight: normal; 149 | } 150 | 151 | .menu__link--sublist { 152 | color: var(--ifm-font-base-color); 153 | font-weight: var(--ifm-font-weight-semibold); 154 | } 155 | 156 | .menu__link--active:not(.menu__link--sublist) { 157 | background-color: var(--ifm-color-primary); 158 | } 159 | 160 | .menu__link--active:not(.menu__link--sublist) { 161 | color: var(--ifm-menu-color-active); 162 | } 163 | 164 | .menu .menu__link.menu__link--sublist:after { 165 | transform: rotateZ(180deg); 166 | -webkit-transition: -webkit-transform 0.2s linear; 167 | transition: -webkit-transform 0.2s linear; 168 | transition-property: transform, -webkit-transform; 169 | transition-duration: 0.2s, 0.2s; 170 | transition-timing-function: linear, linear; 171 | transition-delay: 0s, 0s; 172 | transition: transform 0.2s linear, -webkit-transform 0.2s linear; 173 | color: var(--ifm-font-base-color); 174 | } 175 | 176 | .menu .menu__list-item.menu__list-item--collapsed .menu__link--sublist:after { 177 | transform: rotateZ(90deg); 178 | } 179 | 180 | .codesandbox { 181 | width: 100%; 182 | height: 500px; 183 | border: 0; 184 | border-radius: 4px; 185 | overflow: hidden; 186 | } 187 | 188 | .admonition { 189 | color: black; 190 | border-radius: var(--ifm-global-radius); 191 | border-left: 6px solid var(--ra-admonition-color-dark); 192 | } 193 | 194 | .admonition.admonition-note, 195 | .admonition.admonition-info, 196 | .admonition.admonition-important, 197 | .admonition.admonition-secondary { 198 | --ra-admonition-color: #ecf4f9; 199 | background-color: var(--ra-admonition-color); 200 | } 201 | 202 | .admonition.admonition-success, 203 | .admonition.admonition-tip { 204 | background-color: var(--ra-admonition-color-success); 205 | border-left-color: var(--ra-admonition-color-success-dark); 206 | } 207 | 208 | .admonition.admonition-caution { 209 | background-color: var(--ra-admonition-color-caution); 210 | border-left-color: var(--ra-admonition-color-caution-dark); 211 | } 212 | 213 | .admonition.admonition-warning, 214 | .admonition.admonition-danger { 215 | background-color: var(--ra-admonition-color-error); 216 | border-left-color: var(--ra-admonition-color-error-dark); 217 | } 218 | 219 | .admonition .admonition-icon svg { 220 | fill: black; 221 | stroke: black; 222 | } 223 | 224 | table.checkbox-table tbody td { 225 | text-align: center; 226 | vertical-align: center; 227 | } 228 | 229 | .diagonal-cell { 230 | background: url("data:image/svg+xml;utf8,"); 231 | background-size: 100% 100%; 232 | } 233 | 234 | .diagonal-cell--content { 235 | display: grid; 236 | width: max-content; 237 | justify-content: space-between; 238 | grid-template-columns: repeat(2, 1fr); 239 | grid-auto-rows: 1fr; 240 | } 241 | 242 | .diagonal-cell--topRight { 243 | grid-column-start: 2; 244 | text-align: right; 245 | } 246 | 247 | .diagonal-cell--bottomLeft { 248 | grid-column-start: 1; 249 | } 250 | 251 | .migration-guide .typescript-only h4:after { 252 | content: ' TYPESCRIPT'; 253 | color: var(--ifm-color-info); 254 | position: relative; 255 | } -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /website/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from '@docusaurus/Link' 2 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext' 3 | import HomepageFeatures from '@site/src/components/HomepageFeatures' 4 | import Heading from '@theme/Heading' 5 | import Layout from '@theme/Layout' 6 | import clsx from 'clsx' 7 | import type { FC } from 'react' 8 | import { memo } from 'react' 9 | import styles from './index.module.css' 10 | 11 | const HomepageHeader: FC = memo(() => { 12 | const { siteConfig } = useDocusaurusContext() 13 | return ( 14 |
15 |
16 | 17 | {siteConfig.title} 18 | 19 |

{siteConfig.tagline}

20 |
21 | 25 | Get Started 26 | 27 |
28 |
29 |
30 | ) 31 | }) 32 | 33 | const Home: FC = () => { 34 | const { siteConfig } = useDocusaurusContext() 35 | return ( 36 | 40 | 41 |
42 | 43 |
44 |
45 | ) 46 | } 47 | 48 | export default memo(Home) 49 | -------------------------------------------------------------------------------- /website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/reselect/7435743fb142afd0ae3cce65af20495f1466cb36/website/static/.nojekyll -------------------------------------------------------------------------------- /website/static/img/diagrams/normal-memoization-function.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /website/static/img/diagrams/reselect-memoization.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /website/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/reselect/7435743fb142afd0ae3cce65af20495f1466cb36/website/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /website/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/reselect/7435743fb142afd0ae3cce65af20495f1466cb36/website/static/img/docusaurus.png -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/reselect/7435743fb142afd0ae3cce65af20495f1466cb36/website/static/img/favicon.ico -------------------------------------------------------------------------------- /website/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/img/normal-memoization-function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/reselect/7435743fb142afd0ae3cce65af20495f1466cb36/website/static/img/normal-memoization-function.png -------------------------------------------------------------------------------- /website/static/img/reselect-memoization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reduxjs/reselect/7435743fb142afd0ae3cce65af20495f1466cb36/website/static/img/reselect-memoization.png -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | }, 7 | "ts-node": { 8 | "compilerOptions": { 9 | "module": "CommonJS" 10 | } 11 | } 12 | } 13 | --------------------------------------------------------------------------------