├── .codesandbox └── ci.json ├── .github ├── DISCUSSION_TEMPLATE │ └── bug-report.yml ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── pull_request_template.md └── workflows │ ├── compressed-size.yml │ ├── docs.yml │ ├── preview-release.yml │ ├── publish.yml │ ├── test-multiple-builds.yml │ ├── test-multiple-versions.yml │ ├── test-old-typescript.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── CONTRIBUTING.md ├── FUNDING.json ├── LICENSE ├── README.md ├── docs ├── apis │ ├── create-store.md │ ├── create-with-equality-fn.md │ ├── create.md │ └── shallow.md ├── bear.jpg ├── favicon.ico ├── getting-started │ ├── comparison.md │ └── introduction.md ├── guides │ ├── auto-generating-selectors.md │ ├── connect-to-state-with-url-hash.md │ ├── event-handler-in-pre-react-18.md │ ├── flux-inspired-practice.md │ ├── how-to-reset-state.md │ ├── immutable-state-and-merging.md │ ├── initialize-state-with-props.md │ ├── maps-and-sets-usage.md │ ├── nextjs.md │ ├── practice-with-no-store-actions.md │ ├── prevent-rerenders-with-use-shallow.md │ ├── slices-pattern.md │ ├── ssr-and-hydration.md │ ├── testing.md │ ├── tutorial-tic-tac-toe.md │ ├── typescript.md │ └── updating-state.md ├── hooks │ ├── use-shallow.md │ ├── use-store-with-equality-fn.md │ └── use-store.md ├── integrations │ ├── immer-middleware.md │ ├── persisting-store-data.md │ └── third-party-libraries.md ├── middlewares │ ├── combine.md │ ├── devtools.md │ ├── immer.md │ ├── persist.md │ ├── redux.md │ └── subscribe-with-selector.md ├── migrations │ ├── migrating-to-v4.md │ └── migrating-to-v5.md └── previous-versions │ └── zustand-v3-create-context.md ├── eslint.config.mjs ├── examples ├── demo │ ├── .gitignore │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ ├── ogimage.jpg │ │ ├── robots.txt │ │ └── vite.svg │ ├── src │ │ ├── App.jsx │ │ ├── components │ │ │ ├── CodePreview.jsx │ │ │ ├── CopyButton.jsx │ │ │ ├── Details.jsx │ │ │ ├── Fireflies.jsx │ │ │ ├── Scene.jsx │ │ │ └── SnippetLang.jsx │ │ ├── main.jsx │ │ ├── materials │ │ │ └── layerMaterial.js │ │ ├── pmndrs.css │ │ ├── resources │ │ │ ├── bear.png │ │ │ ├── bg.jpg │ │ │ ├── ground.png │ │ │ ├── javascript-code.js │ │ │ ├── leaves1.png │ │ │ ├── leaves2.png │ │ │ ├── stars.png │ │ │ └── typescript-code.js │ │ ├── styles.css │ │ └── utils │ │ │ └── copy-to-clipboard.js │ └── vite.config.js └── starter │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ ├── assets │ │ └── zustand-mascot.svg │ ├── index.css │ ├── index.tsx │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── rollup.config.mjs ├── src ├── index.ts ├── middleware.ts ├── middleware │ ├── combine.ts │ ├── devtools.ts │ ├── immer.ts │ ├── persist.ts │ ├── redux.ts │ └── subscribeWithSelector.ts ├── react.ts ├── react │ └── shallow.ts ├── shallow.ts ├── traditional.ts ├── types.d.ts ├── vanilla.ts └── vanilla │ └── shallow.ts ├── tests ├── basic.test.tsx ├── devtools.test.tsx ├── middlewareTypes.test.tsx ├── persistAsync.test.tsx ├── persistSync.test.tsx ├── setup.ts ├── shallow.test.tsx ├── ssr.test.tsx ├── subscribe.test.tsx ├── test-utils.ts ├── types.test.tsx └── vanilla │ ├── basic.test.ts │ ├── shallow.test.tsx │ └── subscribe.test.tsx ├── tsconfig.json └── vitest.config.mts /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["dist"], 3 | "sandboxes": [ 4 | "new", 5 | "react-typescript-react-ts", 6 | "simple-react-browserify-x9yni", 7 | "simple-snowpack-react-o1gmx", 8 | "react-parcel-onewf", 9 | "next-js-uo1h0", 10 | "pavlobu-zustand-demo-frutec" 11 | ], 12 | "node": "18" 13 | } 14 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | labels: ['bug'] 2 | body: 3 | - type: markdown 4 | attributes: 5 | value: If you don't have a reproduction link, please choose a different category. 6 | - type: textarea 7 | attributes: 8 | label: Bug Description 9 | description: Describe the bug you encountered 10 | validations: 11 | required: true 12 | - type: input 13 | attributes: 14 | label: Reproduction Link 15 | description: A link to a [TypeScript Playground](https://www.typescriptlang.org/play), a [StackBlitz Project](https://stackblitz.com/) or something else with a minimal reproduction. 16 | validations: 17 | required: true 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [dai-shi] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: [ 14 | 'https://daishi.gumroad.com/l/uaxms', 15 | 'https://daishi.gumroad.com/l/learn-zustand-v4', 16 | ] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Assigned issue 3 | about: This is to create a new issue that already has an assignee. Please open a new discussion otherwise. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Bug Reports 4 | url: https://github.com/pmndrs/zustand/discussions/new?category=bug-report 5 | about: Please post bug reports here. 6 | - name: Questions 7 | url: https://github.com/pmndrs/zustand/discussions/new?category=q-a 8 | about: Please post questions here. 9 | - name: Other Discussions 10 | url: https://github.com/pmndrs/zustand/discussions/new/choose 11 | about: Please post ideas and general discussions here. 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Related Bug Reports or Discussions 2 | 3 | Fixes # 4 | 5 | ## Summary 6 | 7 | ## Check List 8 | 9 | - [ ] `pnpm run fix` for formatting and linting code and docs 10 | -------------------------------------------------------------------------------- /.github/workflows/compressed-size.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | compressed_size: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: pnpm/action-setup@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 'lts/*' 14 | cache: 'pnpm' 15 | - uses: preactjs/compressed-size-action@v2 16 | with: 17 | pattern: './dist/**/*.{js,mjs}' 18 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build documentation and deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | # Cancel previous run (see: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency) 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | uses: pmndrs/docs/.github/workflows/build.yml@v2 16 | with: 17 | mdx: 'docs' 18 | libname: 'Zustand' 19 | home_redirect: '/getting-started/introduction' 20 | icon: '/favicon.ico' 21 | logo: '/bear.jpg' 22 | github: 'https://github.com/pmndrs/zustand' 23 | 24 | deploy: 25 | needs: build 26 | runs-on: ubuntu-latest 27 | 28 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 29 | permissions: 30 | pages: write # to deploy to Pages 31 | id-token: write # to verify the deployment originates from an appropriate source 32 | 33 | # Deploy to the github-pages environment 34 | environment: 35 | name: github-pages 36 | url: ${{ steps.deployment.outputs.page_url }} 37 | 38 | steps: 39 | - id: deployment 40 | uses: actions/deploy-pages@v4 41 | -------------------------------------------------------------------------------- /.github/workflows/preview-release.yml: -------------------------------------------------------------------------------- 1 | name: Preview Release 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | preview_release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: pnpm/action-setup@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 'lts/*' 14 | cache: 'pnpm' 15 | - run: pnpm install 16 | - run: pnpm run build 17 | - run: pnpm dlx pkg-pr-new publish './dist' --compact --template './examples/*' 18 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: pnpm/action-setup@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 'lts/*' 16 | registry-url: 'https://registry.npmjs.org' 17 | cache: 'pnpm' 18 | - run: pnpm install 19 | - run: pnpm run build 20 | - run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | working-directory: dist 24 | -------------------------------------------------------------------------------- /.github/workflows/test-multiple-builds.yml: -------------------------------------------------------------------------------- 1 | name: Test Multiple Builds 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | test_multiple_builds: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | build: [cjs, esm] 16 | env: [development] # [development, production] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: pnpm/action-setup@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 'lts/*' 23 | cache: 'pnpm' 24 | - run: pnpm install 25 | - run: pnpm run build 26 | - name: Patch for DEV-ONLY 27 | if: ${{ matrix.env == 'development' }} 28 | run: | 29 | sed -i~ "s/it[.a-zA-Z]*('\[DEV-ONLY\]/it('/" tests/*.tsx 30 | sed -i~ "s/it[.a-zA-Z]*('\[PRD-ONLY\]/it.skip('/" tests/*.tsx 31 | - name: Patch for PRD-ONLY 32 | if: ${{ matrix.env == 'production' }} 33 | run: | 34 | sed -i~ "s/it[.a-zA-Z]*('\[PRD-ONLY\]/it('/" tests/*.tsx 35 | sed -i~ "s/it[.a-zA-Z]*('\[DEV-ONLY\]/it.skip('/" tests/*.tsx 36 | - name: Patch for CJS 37 | if: ${{ matrix.build == 'cjs' }} 38 | run: | 39 | sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\1.js')/" vitest.config.mts 40 | - name: Patch for ESM 41 | if: ${{ matrix.build == 'esm' }} 42 | run: | 43 | sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\/esm\1.mjs')/" vitest.config.mts 44 | sed -i~ "1s/^/import.meta.env=import.meta.env||{};import.meta.env.MODE='${NODE_ENV}';/" tests/*.tsx 45 | env: 46 | NODE_ENV: ${{ matrix.env }} 47 | - name: Test ${{ matrix.build }} ${{ matrix.env }} 48 | run: | 49 | pnpm run test:spec 50 | env: 51 | NODE_ENV: ${{ matrix.env }} 52 | -------------------------------------------------------------------------------- /.github/workflows/test-multiple-versions.yml: -------------------------------------------------------------------------------- 1 | name: Test Multiple Versions 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | test_multiple_versions: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | react: 16 | - 18.0.0 17 | - 18.1.0 18 | - 18.2.0 19 | - 18.3.1 20 | - 19.0.0-rc.1 21 | - 19.1.0-canary-3ce77d55-20250106 22 | - 0.0.0-experimental-3ce77d55-20250106 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: pnpm/action-setup@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 'lts/*' 29 | cache: 'pnpm' 30 | - run: pnpm install 31 | - name: Test ${{ matrix.react }} ${{ matrix.devtools-skip }} 32 | run: | 33 | pnpm add -D react@${{ matrix.react }} react-dom@${{ matrix.react }} 34 | pnpm run test:spec 35 | -------------------------------------------------------------------------------- /.github/workflows/test-old-typescript.yml: -------------------------------------------------------------------------------- 1 | name: Test Old TypeScript 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | test_old_typescript: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | typescript: 16 | - 5.5.4 17 | - 5.4.5 18 | - 5.3.3 19 | - 5.2.2 20 | - 5.1.6 21 | - 5.0.4 22 | - 4.9.5 23 | - 4.8.4 24 | - 4.7.4 25 | - 4.6.4 26 | - 4.5.5 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: pnpm/action-setup@v4 30 | - uses: actions/setup-node@v4 31 | with: 32 | node-version: 'lts/*' 33 | cache: 'pnpm' 34 | - run: pnpm install 35 | - run: pnpm run build 36 | - name: Patch for all TS 37 | run: | 38 | sed -i~ 's/"isolatedDeclarations": true,//' tsconfig.json 39 | - name: Patch for v4/v3 TS 40 | if: ${{ startsWith(matrix.typescript, '4.') || startsWith(matrix.typescript, '3.') }} 41 | run: | 42 | sed -i~ 's/"verbatimModuleSyntax": true,//' tsconfig.json 43 | - name: Patch for Old TS 44 | run: | 45 | sed -i~ 's/"moduleResolution": "bundler",/"moduleResolution": "node",/' tsconfig.json 46 | sed -i~ 's/"allowImportingTsExtensions": true,//' tsconfig.json 47 | sed -i~ 's/"zustand": \["\.\/src\/index\.ts"\],/"zustand": [".\/dist\/index.d.ts"],/' tsconfig.json 48 | sed -i~ 's/"zustand\/\*": \["\.\/src\/\*\.ts"\]/"zustand\/*": [".\/dist\/*.d.ts"]/' tsconfig.json 49 | sed -i~ 's/"include": .*/"include": ["src\/types.d.ts", "dist\/**\/*", "tests\/**\/*"],/' tsconfig.json 50 | pnpm json -I -f package.json -e "this.resolutions={}; this.resolutions['@types/node']='18.13.0';" 51 | pnpm add -D @types/node@18.13.0 52 | - name: Install old TypeScript 53 | run: pnpm add -D typescript@${{ matrix.typescript }} 54 | - name: Test ${{ matrix.typescript }} 55 | run: pnpm run test:types 56 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: pnpm/action-setup@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 'lts/*' 18 | cache: 'pnpm' 19 | - run: pnpm install 20 | - run: pnpm run test:format 21 | - run: pnpm run test:types 22 | - run: pnpm run test:lint 23 | - run: pnpm run test:spec 24 | - run: pnpm run build # we don't have any other workflows to test build 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | Thumbs.db 4 | ehthumbs.db 5 | Desktop.ini 6 | $RECYCLE.BIN/ 7 | .DS_Store 8 | .vscode 9 | .docz/ 10 | coverage/ 11 | .rpt2_cache/ 12 | .idea 13 | examples/**/*/package-lock.json 14 | examples/**/*/yarn.lock 15 | examples/**/*/pnpm-lock.yaml 16 | examples/**/*/bun.lockb 17 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | pnpm-lock.yaml 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## General Guideline 4 | 5 | ### Reporting Issues 6 | 7 | If you have found what you think is a bug, please [start a discussion](https://github.com/pmndrs/zustand/discussions/new?category=bug-report). 8 | 9 | For any usage questions, please [start a discussion](https://github.com/pmndrs/zustand/discussions/new?category=q-a). 10 | 11 | ### Suggesting New Features 12 | 13 | If you are here to suggest a feature, first [start a discussion](https://github.com/pmndrs/zustand/discussions/new?category=ideas) if it does not already exist. From there, we will discuss use-cases for the feature and then finally discuss how it could be implemented. 14 | 15 | ### Committing 16 | 17 | We are applying [conventional commit spec](https://www.conventionalcommits.org/en/v1.0.0/) here. In short, that means a commit has to be one of the following types: 18 | 19 | Your commit type must be one of the following: 20 | 21 | - **feat**: A new feature. 22 | - **fix**: A bug fix. 23 | - **refactor**: A code change that neither fixes a bug nor adds a feature. 24 | - **chore**: Changes to the build process, configuration, dependencies, CI/CD pipelines, or other auxiliary tools and libraries. 25 | - **docs**: Documentation-only changes. 26 | - **test**: Adding missing or correcting existing tests. 27 | 28 | If you are unfamiliar with the usage of conventional commits, 29 | the short version is to simply specify the type as a first word, 30 | and follow it with a colon and a space, then start your message 31 | from a lowercase letter, like this: 32 | 33 | ``` 34 | feat: add a 'foo' type support 35 | ``` 36 | 37 | You can also specify the scope of the commit in the parentheses after a type: 38 | 39 | ``` 40 | fix(react): change the 'bar' parameter type 41 | ``` 42 | 43 | ### Development 44 | 45 | If you would like to contribute by fixing an open issue or developing a new feature you can use this suggested workflow: 46 | 47 | #### General 48 | 49 | 1. Fork this repository. 50 | 2. Create a new feature branch based off the `main` branch. 51 | 3. Follow the [Core](#Core) and/or the [Documentation](#Documentation) guide below and come back to this once done. 52 | 4. Run `pnpm run fix:format` to format the code. 53 | 5. Git stage your required changes and commit (review the commit guidelines below). 54 | 6. Submit the PR for review. 55 | 56 | ##### Core 57 | 58 | 1. Run `npm install` to install dependencies. 59 | 2. Create failing tests for your fix or new feature in the [`tests`](./tests/) folder. 60 | 3. Implement your changes. 61 | 4. Run `pnpm run build` to build the library. _(Pro-tip: `pnpm run build-watch` runs the build in watch mode)_ 62 | 5. Run the tests by running `pnpm run test` and ensure that they pass. 63 | 6. You can use `pnpm link` to sym-link this package and test it locally on your own project. Alternatively, you may use CodeSandbox CI's canary releases to test the changes in your own project. (requires a PR to be created first) 64 | 7. Follow step 4 and onwards from the [General](#General) guide above to bring it to the finish line. 65 | 66 | ### Pull Requests 67 | 68 | Please try to keep your pull request focused in scope and avoid including unrelated commits. 69 | 70 | After you have submitted your pull request, we'll try to get back to you as soon as possible. We may suggest some changes or request improvements, therefore, please check ✅ ["Allow edits from maintainers"](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) on your PR. 71 | 72 | ## Zustand-specific Guideline 73 | 74 | ##### Documentation 75 | 76 | Our [docs](https://zustand.docs.pmnd.rs) are based on [`pmndrs/docs`](https://github.com/pmndrs/docs). 77 | 78 | 1. Separately, clone the `pmndrs/docs`. (you don't need to fork it). 79 | 2. Inside the `pmndrs/docs` directory: 80 | 1. Create a `.env` file in the root directory with the next environment variables: `MDX=docs/zustand/docs` and `HOME_REDIRECT=/getting-started/introduction`. 81 | 2. Run `npm install` to install dependencies. 82 | 3. Run `npm run dev` to start the dev server. 83 | 4. Navigate to [`http://localhost:3000`](http://localhost:3000) to view the documents. 84 | 3. Go Back to the forked repository: 85 | 1. Run `pnpm install` to install dependencies. 86 | 2. Navigate to the [`docs`](./docs/) folder and make necessary changes to the documents. 87 | 3. Add your changes to the documents and see them live reloaded in the browser. (if you don't see changes, try `control + c`, then run `npm run dev` in the cloned `pnmdrs/docs` repository) 88 | 4. Follow step 4 and onwards from the [General](#General) guide above to bring it to the finish line. 89 | 90 | Thank you for contributing! :heart: 91 | -------------------------------------------------------------------------------- /FUNDING.json: -------------------------------------------------------------------------------- 1 | { 2 | "drips": { 3 | "ethereum": { 4 | "ownedBy": "0xBA918e34bed77Ba7a9fCF53be0A81FA538d56FA7" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Paul Henschel 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 | -------------------------------------------------------------------------------- /docs/apis/shallow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: shallow 3 | description: How compare simple data effectively 4 | nav: 27 5 | --- 6 | 7 | `shallow` lets you run fast checks on simple data structures. It effectively identifies changes in 8 | **top-level** properties when you're working with data structures that don't have nested objects or 9 | arrays within them. 10 | 11 | > [!NOTE] 12 | > Shallow lets you perform quick comparisons, but keep its limitations in mind. 13 | 14 | ```js 15 | const equal = shallow(a, b) 16 | ``` 17 | 18 | - [Types](#types) 19 | - [Signature](#shallow-signature) 20 | - [Reference](#reference) 21 | - [Usage](#usage) 22 | - [Comparing Primitives](#comparing-primitives) 23 | - [Comparing Objects](#comparing-objects) 24 | - [Comparing Sets](#comparing-sets) 25 | - [Comparing Maps](#comparing-maps) 26 | - [Troubleshooting](#troubleshooting) 27 | - [Comparing objects returns `false` even if they are identical.](#comparing-objects-returns-false-even-if-they-are-identical) 28 | 29 | ## Types 30 | 31 | ### Signature 32 | 33 | ```ts 34 | shallow(a: T, b: T): boolean 35 | ``` 36 | 37 | ## Reference 38 | 39 | ### `shallow(a, b)` 40 | 41 | #### Parameters 42 | 43 | - `a`: The first value. 44 | - `b`: The second value. 45 | 46 | #### Returns 47 | 48 | `shallow` returns `true` when `a` and `b` are equal based on a shallow comparison of their 49 | **top-level** properties. Otherwise, it should return `false`. 50 | 51 | ## Usage 52 | 53 | ### Comparing Primitives 54 | 55 | When comparing primitive values like `string`s, `number`s, `boolean`s, and `BigInt`s, both 56 | `Object.is` and `shallow` function return `true` if the values are the same. This is because 57 | primitive values are compared by their actual value rather than by reference. 58 | 59 | ```ts 60 | const stringLeft = 'John Doe' 61 | const stringRight = 'John Doe' 62 | 63 | Object.is(stringLeft, stringRight) // -> true 64 | shallow(stringLeft, stringRight) // -> true 65 | 66 | const numberLeft = 10 67 | const numberRight = 10 68 | 69 | Object.is(numberLeft, numberRight) // -> true 70 | shallow(numberLeft, numberRight) // -> true 71 | 72 | const booleanLeft = true 73 | const booleanRight = true 74 | 75 | Object.is(booleanLeft, booleanRight) // -> true 76 | shallow(booleanLeft, booleanRight) // -> true 77 | 78 | const bigIntLeft = 1n 79 | const bigIntRight = 1n 80 | 81 | Object.is(bigIntLeft, bigIntRight) // -> true 82 | shallow(bigIntLeft, bigIntRight) // -> true 83 | ``` 84 | 85 | ### Comparing Objects 86 | 87 | When comparing objects, it's important to understand how `Object.is` and `shallow` function 88 | operate, as they handle comparisons differently. 89 | 90 | The `shallow` function returns `true` because shallow performs a shallow comparison of the objects. 91 | It checks if the top-level properties and their values are the same. In this case, the top-level 92 | properties (`firstName`, `lastName`, and `age`) and their values are identical between `objectLeft` 93 | and `objectRight`, so shallow considers them equal. 94 | 95 | ```ts 96 | const objectLeft = { 97 | firstName: 'John', 98 | lastName: 'Doe', 99 | age: 30, 100 | } 101 | const objectRight = { 102 | firstName: 'John', 103 | lastName: 'Doe', 104 | age: 30, 105 | } 106 | 107 | Object.is(objectLeft, objectRight) // -> false 108 | shallow(objectLeft, objectRight) // -> true 109 | ``` 110 | 111 | ### Comparing Sets 112 | 113 | When comparing sets, it's important to understand how `Object.is` and `shallow` function operate, 114 | as they handle comparisons differently. 115 | 116 | The `shallow` function returns `true` because shallow performs a shallow comparison of the sets. It 117 | checks if the top-level properties (in this case, the sets themselves) are the same. Since `setLeft` 118 | and `setRight` are both instances of the Set object and contain the same elements, shallow considers 119 | them equal. 120 | 121 | ```ts 122 | const setLeft = new Set([1, 2, 3]) 123 | const setRight = new Set([1, 2, 3]) 124 | 125 | Object.is(setLeft, setRight) // -> false 126 | shallow(setLeft, setRight) // -> true 127 | ``` 128 | 129 | ### Comparing Maps 130 | 131 | When comparing maps, it's important to understand how `Object.is` and `shallow` function operate, as 132 | they handle comparisons differently. 133 | 134 | The `shallow` returns `true` because shallow performs a shallow comparison of the maps. It checks if 135 | the top-level properties (in this case, the maps themselves) are the same. Since `mapLeft` and 136 | `mapRight` are both instances of the Map object and contain the same key-value pairs, shallow 137 | considers them equal. 138 | 139 | ```ts 140 | const mapLeft = new Map([ 141 | [1, 'one'], 142 | [2, 'two'], 143 | [3, 'three'], 144 | ]) 145 | const mapRight = new Map([ 146 | [1, 'one'], 147 | [2, 'two'], 148 | [3, 'three'], 149 | ]) 150 | 151 | Object.is(mapLeft, mapRight) // -> false 152 | shallow(mapLeft, mapRight) // -> true 153 | ``` 154 | 155 | ## Troubleshooting 156 | 157 | ### Comparing objects returns `false` even if they are identical. 158 | 159 | The `shallow` function performs a shallow comparison. A shallow comparison checks if the top-level 160 | properties of two objects are equal. It does not check nested objects or deeply nested properties. 161 | In other words, it only compares the references of the properties. 162 | 163 | In the following example, the shallow function returns `false` because it compares only the 164 | top-level properties and their references. The address property in both objects is a nested object, 165 | and even though their contents are identical, their references are different. Consequently, shallow 166 | sees them as different, resulting in `false`. 167 | 168 | ```ts 169 | const objectLeft = { 170 | firstName: 'John', 171 | lastName: 'Doe', 172 | age: 30, 173 | address: { 174 | street: 'Kulas Light', 175 | suite: 'Apt. 556', 176 | city: 'Gwenborough', 177 | zipcode: '92998-3874', 178 | geo: { 179 | lat: '-37.3159', 180 | lng: '81.1496', 181 | }, 182 | }, 183 | } 184 | const objectRight = { 185 | firstName: 'John', 186 | lastName: 'Doe', 187 | age: 30, 188 | address: { 189 | street: 'Kulas Light', 190 | suite: 'Apt. 556', 191 | city: 'Gwenborough', 192 | zipcode: '92998-3874', 193 | geo: { 194 | lat: '-37.3159', 195 | lng: '81.1496', 196 | }, 197 | }, 198 | } 199 | 200 | Object.is(objectLeft, objectRight) // -> false 201 | shallow(objectLeft, objectRight) // -> false 202 | ``` 203 | 204 | If we remove the `address` property, the shallow comparison would work as expected because all 205 | top-level properties would be primitive values or references to the same values: 206 | 207 | ```ts 208 | const objectLeft = { 209 | firstName: 'John', 210 | lastName: 'Doe', 211 | age: 30, 212 | } 213 | const objectRight = { 214 | firstName: 'John', 215 | lastName: 'Doe', 216 | age: 30, 217 | } 218 | 219 | Object.is(objectLeft, objectRight) // -> false 220 | shallow(objectLeft, objectRight) // -> true 221 | ``` 222 | 223 | In this modified example, `objectLeft` and `objectRight` have the same top-level properties and 224 | primitive values. Since `shallow` function only compares the top-level properties, it will return 225 | `true` because the primitive values (`firstName`, `lastName`, and `age`) are identical in both 226 | objects. 227 | -------------------------------------------------------------------------------- /docs/bear.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/zustand/4d3a0176ca20e9c58cc8f39157f80fb126d299fb/docs/bear.jpg -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/zustand/4d3a0176ca20e9c58cc8f39157f80fb126d299fb/docs/favicon.ico -------------------------------------------------------------------------------- /docs/getting-started/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: How to use Zustand 4 | nav: 0 5 | --- 6 | 7 |
8 | Logo Zustand 9 |
10 | 11 | A small, fast, and scalable bearbones state management solution. 12 | Zustand has a comfy API based on hooks. 13 | It isn't boilerplatey or opinionated, 14 | but has enough convention to be explicit and flux-like. 15 | 16 | Don't disregard it because it's cute, it has claws! 17 | Lots of time was spent to deal with common pitfalls, 18 | like the dreaded [zombie child problem], 19 | [React concurrency], and [context loss] 20 | between mixed renderers. 21 | It may be the one state manager in the React space that gets all of these right. 22 | 23 | You can try a live demo [here](https://codesandbox.io/s/dazzling-moon-itop4). 24 | 25 | [zombie child problem]: https://react-redux.js.org/api/hooks#stale-props-and-zombie-children 26 | [react concurrency]: https://github.com/bvaughn/rfcs/blob/useMutableSource/text/0000-use-mutable-source.md 27 | [context loss]: https://github.com/facebook/react/issues/13332 28 | 29 | ## Installation 30 | 31 | Zustand is available as a package on NPM for use: 32 | 33 | ```bash 34 | # NPM 35 | npm install zustand 36 | # Or, use any package manager of your choice. 37 | ``` 38 | 39 | ## First create a store 40 | 41 | Your store is a hook! 42 | You can put anything in it: primitives, objects, functions. 43 | The `set` function _merges_ state. 44 | 45 | ```js 46 | import { create } from 'zustand' 47 | 48 | const useStore = create((set) => ({ 49 | bears: 0, 50 | increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), 51 | removeAllBears: () => set({ bears: 0 }), 52 | updateBears: (newBears) => set({ bears: newBears }), 53 | })) 54 | ``` 55 | 56 | ## Then bind your components, and that's it! 57 | 58 | You can use the hook anywhere, without the need of providers. 59 | Select your state and the consuming component 60 | will re-render when that state changes. 61 | 62 | ```jsx 63 | function BearCounter() { 64 | const bears = useStore((state) => state.bears) 65 | return

{bears} bears around here...

66 | } 67 | 68 | function Controls() { 69 | const increasePopulation = useStore((state) => state.increasePopulation) 70 | return 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/guides/auto-generating-selectors.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Auto Generating Selectors 3 | nav: 5 4 | --- 5 | 6 | We recommend using selectors when using either the properties or actions from the store. You can access values from the store like so: 7 | 8 | ```typescript 9 | const bears = useBearStore((state) => state.bears) 10 | ``` 11 | 12 | However, writing these could be tedious. If that is the case for you, you can auto-generate your selectors. 13 | 14 | ## Create the following function: `createSelectors` 15 | 16 | ```typescript 17 | import { StoreApi, UseBoundStore } from 'zustand' 18 | 19 | type WithSelectors = S extends { getState: () => infer T } 20 | ? S & { use: { [K in keyof T]: () => T[K] } } 21 | : never 22 | 23 | const createSelectors = >>( 24 | _store: S, 25 | ) => { 26 | let store = _store as WithSelectors 27 | store.use = {} 28 | for (let k of Object.keys(store.getState())) { 29 | ;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s]) 30 | } 31 | 32 | return store 33 | } 34 | ``` 35 | 36 | If you have a store like this: 37 | 38 | ```typescript 39 | interface BearState { 40 | bears: number 41 | increase: (by: number) => void 42 | increment: () => void 43 | } 44 | 45 | const useBearStoreBase = create()((set) => ({ 46 | bears: 0, 47 | increase: (by) => set((state) => ({ bears: state.bears + by })), 48 | increment: () => set((state) => ({ bears: state.bears + 1 })), 49 | })) 50 | ``` 51 | 52 | Apply that function to your store: 53 | 54 | ```typescript 55 | const useBearStore = createSelectors(useBearStoreBase) 56 | ``` 57 | 58 | Now the selectors are auto generated and you can access them directly: 59 | 60 | ```typescript 61 | // get the property 62 | const bears = useBearStore.use.bears() 63 | 64 | // get the action 65 | const increment = useBearStore.use.increment() 66 | ``` 67 | 68 | ## Vanilla Store 69 | 70 | If you are using a vanilla store, use the following `createSelectors` function: 71 | 72 | ```typescript 73 | import { StoreApi, useStore } from 'zustand' 74 | 75 | type WithSelectors = S extends { getState: () => infer T } 76 | ? S & { use: { [K in keyof T]: () => T[K] } } 77 | : never 78 | 79 | const createSelectors = >(_store: S) => { 80 | const store = _store as WithSelectors 81 | store.use = {} 82 | for (const k of Object.keys(store.getState())) { 83 | ;(store.use as any)[k] = () => 84 | useStore(_store, (s) => s[k as keyof typeof s]) 85 | } 86 | 87 | return store 88 | } 89 | ``` 90 | 91 | The usage is the same as a React store. If you have a store like this: 92 | 93 | ```typescript 94 | import { createStore } from 'zustand' 95 | 96 | interface BearState { 97 | bears: number 98 | increase: (by: number) => void 99 | increment: () => void 100 | } 101 | 102 | const store = createStore((set) => ({ 103 | bears: 0, 104 | increase: (by) => set((state) => ({ bears: state.bears + by })), 105 | increment: () => set((state) => ({ bears: state.bears + 1 })), 106 | })) 107 | ``` 108 | 109 | Apply that function to your store: 110 | 111 | ```typescript 112 | const useBearStore = createSelectors(store) 113 | ``` 114 | 115 | Now the selectors are auto generated and you can access them directly: 116 | 117 | ```typescript 118 | // get the property 119 | const bears = useBearStore.use.bears() 120 | 121 | // get the action 122 | const increment = useBearStore.use.increment() 123 | ``` 124 | 125 | ## Live Demo 126 | 127 | For a working example of this, see the [Code Sandbox](https://codesandbox.io/s/zustand-auto-generate-selectors-forked-rl8v5e?file=/src/selectors.ts). 128 | 129 | ## Third-party Libraries 130 | 131 | - [auto-zustand-selectors-hook](https://github.com/Albert-Gao/auto-zustand-selectors-hook) 132 | - [react-hooks-global-state](https://github.com/dai-shi/react-hooks-global-state) 133 | - [zustood](https://github.com/udecode/zustood) 134 | - [@davstack/store](https://github.com/DawidWraga/davstack) 135 | -------------------------------------------------------------------------------- /docs/guides/connect-to-state-with-url-hash.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Connect to state with URL 3 | nav: 11 4 | --- 5 | 6 | ## Connect State with URL Hash 7 | 8 | If you want to connect state of a store to URL hash, you can create your own hash storage. 9 | 10 | ```ts 11 | import { create } from 'zustand' 12 | import { persist, StateStorage, createJSONStorage } from 'zustand/middleware' 13 | 14 | const hashStorage: StateStorage = { 15 | getItem: (key): string => { 16 | const searchParams = new URLSearchParams(location.hash.slice(1)) 17 | const storedValue = searchParams.get(key) ?? '' 18 | return JSON.parse(storedValue) 19 | }, 20 | setItem: (key, newValue): void => { 21 | const searchParams = new URLSearchParams(location.hash.slice(1)) 22 | searchParams.set(key, JSON.stringify(newValue)) 23 | location.hash = searchParams.toString() 24 | }, 25 | removeItem: (key): void => { 26 | const searchParams = new URLSearchParams(location.hash.slice(1)) 27 | searchParams.delete(key) 28 | location.hash = searchParams.toString() 29 | }, 30 | } 31 | 32 | export const useBoundStore = create( 33 | persist( 34 | (set, get) => ({ 35 | fishes: 0, 36 | addAFish: () => set({ fishes: get().fishes + 1 }), 37 | }), 38 | { 39 | name: 'food-storage', // unique name 40 | storage: createJSONStorage(() => hashStorage), 41 | }, 42 | ), 43 | ) 44 | ``` 45 | 46 | ### CodeSandbox Demo 47 | 48 | https://codesandbox.io/s/zustand-state-with-url-hash-demo-f29b88?file=/src/store/index.ts 49 | 50 | ## Persist and Connect State with URL Parameters (Example: URL Query Parameters) 51 | 52 | There are times when you want to conditionally connect the state to the URL. 53 | This example depicts usage of the URL query parameters 54 | while keeping it synced with another persistence implementation, like `localstorage`. 55 | 56 | If you want the URL params to always populate, the conditional check on `getUrlSearch()` can be removed. 57 | 58 | The implementation below will update the URL in place, without refresh, as the relevant states change. 59 | 60 | ```ts 61 | import { create } from 'zustand' 62 | import { persist, StateStorage, createJSONStorage } from 'zustand/middleware' 63 | 64 | const getUrlSearch = () => { 65 | return window.location.search.slice(1) 66 | } 67 | 68 | const persistentStorage: StateStorage = { 69 | getItem: (key): string => { 70 | // Check URL first 71 | if (getUrlSearch()) { 72 | const searchParams = new URLSearchParams(getUrlSearch()) 73 | const storedValue = searchParams.get(key) 74 | return JSON.parse(storedValue as string) 75 | } else { 76 | // Otherwise, we should load from localstorage or alternative storage 77 | return JSON.parse(localStorage.getItem(key) as string) 78 | } 79 | }, 80 | setItem: (key, newValue): void => { 81 | // Check if query params exist at all, can remove check if always want to set URL 82 | if (getUrlSearch()) { 83 | const searchParams = new URLSearchParams(getUrlSearch()) 84 | searchParams.set(key, JSON.stringify(newValue)) 85 | window.history.replaceState(null, '', `?${searchParams.toString()}`) 86 | } 87 | 88 | localStorage.setItem(key, JSON.stringify(newValue)) 89 | }, 90 | removeItem: (key): void => { 91 | const searchParams = new URLSearchParams(getUrlSearch()) 92 | searchParams.delete(key) 93 | window.location.search = searchParams.toString() 94 | }, 95 | } 96 | 97 | type LocalAndUrlStore = { 98 | typesOfFish: string[] 99 | addTypeOfFish: (fishType: string) => void 100 | numberOfBears: number 101 | setNumberOfBears: (newNumber: number) => void 102 | } 103 | 104 | const storageOptions = { 105 | name: 'fishAndBearsStore', 106 | storage: createJSONStorage(() => persistentStorage), 107 | } 108 | 109 | const useLocalAndUrlStore = create( 110 | persist( 111 | (set) => ({ 112 | typesOfFish: [], 113 | addTypeOfFish: (fishType) => 114 | set((state) => ({ typesOfFish: [...state.typesOfFish, fishType] })), 115 | 116 | numberOfBears: 0, 117 | setNumberOfBears: (numberOfBears) => set(() => ({ numberOfBears })), 118 | }), 119 | storageOptions, 120 | ), 121 | ) 122 | 123 | export default useLocalAndUrlStore 124 | ``` 125 | 126 | When generating the URL from a component, you can call buildShareableUrl: 127 | 128 | ```ts 129 | const buildURLSuffix = (params, version = 0) => { 130 | const searchParams = new URLSearchParams() 131 | 132 | const zustandStoreParams = { 133 | state: { 134 | typesOfFish: params.typesOfFish, 135 | numberOfBears: params.numberOfBears, 136 | }, 137 | version: version, // version is here because that is included with how Zustand sets the state 138 | } 139 | 140 | // The URL param key should match the name of the store, as specified as in storageOptions above 141 | searchParams.set('fishAndBearsStore', JSON.stringify(zustandStoreParams)) 142 | return searchParams.toString() 143 | } 144 | 145 | export const buildShareableUrl = (params, version) => { 146 | return `${window.location.origin}?${buildURLSuffix(params, version)}` 147 | } 148 | ``` 149 | 150 | The generated URL would look like (here without any encoding, for readability): 151 | 152 | `https://localhost/search?fishAndBearsStore={"state":{"typesOfFish":["tilapia","salmon"],"numberOfBears":15},"version":0}}` 153 | -------------------------------------------------------------------------------- /docs/guides/event-handler-in-pre-react-18.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Calling actions outside a React event handler in pre React 18 3 | nav: 9 4 | --- 5 | 6 | Because React handles `setState` synchronously if it's called outside an event handler, updating the state outside an event handler will force react to update the components synchronously. Therefore, there is a risk of encountering the zombie-child effect. 7 | In order to fix this, the action needs to be wrapped in `unstable_batchedUpdates` like so: 8 | 9 | ```jsx 10 | import { unstable_batchedUpdates } from 'react-dom' // or 'react-native' 11 | 12 | const useFishStore = create((set) => ({ 13 | fishes: 0, 14 | increaseFishes: () => set((prev) => ({ fishes: prev.fishes + 1 })), 15 | })) 16 | 17 | const nonReactCallback = () => { 18 | unstable_batchedUpdates(() => { 19 | useFishStore.getState().increaseFishes() 20 | }) 21 | } 22 | ``` 23 | 24 | More details: https://github.com/pmndrs/zustand/issues/302 25 | -------------------------------------------------------------------------------- /docs/guides/flux-inspired-practice.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Flux inspired practice 3 | nav: 4 4 | --- 5 | 6 | Although Zustand is an unopinionated library, we do recommend a few patterns. 7 | These are inspired by practices originally found in [Flux](https://github.com/facebookarchive/flux), 8 | and more recently [Redux](https://redux.js.org/understanding/thinking-in-redux/three-principles), 9 | so if you are coming from another library, you should feel right at home. 10 | 11 | However, Zustand does differ in some fundamental ways, 12 | so some terminology may not perfectly align to other libraries. 13 | 14 | ## Recommended patterns 15 | 16 | ### Single store 17 | 18 | Your applications global state should be located in a single Zustand store. 19 | 20 | If you have a large application, Zustand supports [splitting the store into slices](./slices-pattern.md). 21 | 22 | ### Use `set` / `setState` to update the store 23 | 24 | Always use `set` (or `setState`) to perform updates to your store. 25 | `set` (and `setState`) ensures the described update is correctly merged and listeners are appropriately notified. 26 | 27 | ### Colocate store actions 28 | 29 | In Zustand, state can be updated without the use of dispatched actions and reducers found in other Flux libraries. 30 | These store actions can be added directly to the store as shown below. 31 | 32 | Optionally, by using `setState` they can be [located external to the store](./practice-with-no-store-actions.md) 33 | 34 | ```js 35 | const useBoundStore = create((set) => ({ 36 | storeSliceA: ..., 37 | storeSliceB: ..., 38 | storeSliceC: ..., 39 | updateX: () => set(...), 40 | updateY: () => set(...), 41 | })) 42 | ``` 43 | 44 | ## Redux-like patterns 45 | 46 | If you can't live without Redux-like reducers, you can define a `dispatch` function on the root level of the store: 47 | 48 | ```typescript 49 | const types = { increase: 'INCREASE', decrease: 'DECREASE' } 50 | 51 | const reducer = (state, { type, by = 1 }) => { 52 | switch (type) { 53 | case types.increase: 54 | return { grumpiness: state.grumpiness + by } 55 | case types.decrease: 56 | return { grumpiness: state.grumpiness - by } 57 | } 58 | } 59 | 60 | const useGrumpyStore = create((set) => ({ 61 | grumpiness: 0, 62 | dispatch: (args) => set((state) => reducer(state, args)), 63 | })) 64 | 65 | const dispatch = useGrumpyStore((state) => state.dispatch) 66 | dispatch({ type: types.increase, by: 2 }) 67 | ``` 68 | 69 | You could also use our redux-middleware. It wires up your main reducer, sets initial state, and adds a dispatch function to the state itself and the vanilla api. 70 | 71 | ```typescript 72 | import { redux } from 'zustand/middleware' 73 | 74 | const useReduxStore = create(redux(reducer, initialState)) 75 | ``` 76 | 77 | Another way to update the store could be through functions wrapping the state functions. These could also handle side-effects of actions. For example, with HTTP-calls. To use Zustand in a non-reactive way, see [the readme](https://github.com/pmndrs/zustand#readingwriting-state-and-reacting-to-changes-outside-of-components). 78 | -------------------------------------------------------------------------------- /docs/guides/how-to-reset-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to reset state 3 | nav: 12 4 | --- 5 | 6 | The following pattern can be used to reset the state to its initial value. 7 | 8 | ```ts 9 | import { create } from 'zustand' 10 | 11 | // define types for state values and actions separately 12 | type State = { 13 | salmon: number 14 | tuna: number 15 | } 16 | 17 | type Actions = { 18 | addSalmon: (qty: number) => void 19 | addTuna: (qty: number) => void 20 | reset: () => void 21 | } 22 | 23 | // define the initial state 24 | const initialState: State = { 25 | salmon: 0, 26 | tuna: 0, 27 | } 28 | 29 | // create store 30 | const useSlice = create()((set, get) => ({ 31 | ...initialState, 32 | addSalmon: (qty: number) => { 33 | set({ salmon: get().salmon + qty }) 34 | }, 35 | addTuna: (qty: number) => { 36 | set({ tuna: get().tuna + qty }) 37 | }, 38 | reset: () => { 39 | set(initialState) 40 | }, 41 | })) 42 | ``` 43 | 44 | Resetting multiple stores at once 45 | 46 | ```ts 47 | import type { StateCreator } from 'zustand' 48 | import { create: actualCreate } from 'zustand' 49 | 50 | const storeResetFns = new Set<() => void>() 51 | 52 | const resetAllStores = () => { 53 | storeResetFns.forEach((resetFn) => { 54 | resetFn() 55 | }) 56 | } 57 | 58 | export const create = (() => { 59 | return (stateCreator: StateCreator) => { 60 | const store = actualCreate(stateCreator) 61 | const initialState = store.getInitialState() 62 | storeResetFns.add(() => { 63 | store.setState(initialState, true) 64 | }) 65 | return store 66 | } 67 | }) as typeof actualCreate 68 | ``` 69 | 70 | ## CodeSandbox Demo 71 | 72 | - Basic: https://codesandbox.io/s/zustand-how-to-reset-state-basic-demo-rrqyon 73 | - Advanced: https://codesandbox.io/s/zustand-how-to-reset-state-advanced-demo-gtu0qe 74 | - Immer: https://codesandbox.io/s/how-to-reset-state-advance-immer-demo-nyet3f 75 | -------------------------------------------------------------------------------- /docs/guides/immutable-state-and-merging.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Immutable state and merging 3 | nav: 3 4 | --- 5 | 6 | Like with React's `useState`, we need to update state immutably. 7 | 8 | Here's a typical example: 9 | 10 | ```jsx 11 | import { create } from 'zustand' 12 | 13 | const useCountStore = create((set) => ({ 14 | count: 0, 15 | inc: () => set((state) => ({ count: state.count + 1 })), 16 | })) 17 | ``` 18 | 19 | The `set` function is to update state in the store. 20 | Because the state is immutable, it should have been like this: 21 | 22 | ```js 23 | set((state) => ({ ...state, count: state.count + 1 })) 24 | ``` 25 | 26 | However, as this is a common pattern, `set` actually merges state, and 27 | we can skip the `...state` part: 28 | 29 | ```js 30 | set((state) => ({ count: state.count + 1 })) 31 | ``` 32 | 33 | ## Nested objects 34 | 35 | The `set` function merges state at only one level. 36 | If you have a nested object, you need to merge them explicitly. You will use the spread operator pattern like so: 37 | 38 | ```jsx 39 | import { create } from 'zustand' 40 | 41 | const useCountStore = create((set) => ({ 42 | nested: { count: 0 }, 43 | inc: () => 44 | set((state) => ({ 45 | nested: { ...state.nested, count: state.nested.count + 1 }, 46 | })), 47 | })) 48 | ``` 49 | 50 | For complex use cases, consider using some libraries that help with immutable updates. 51 | You can refer to [Updating nested state object values](./updating-state.md#deeply-nested-object). 52 | 53 | ## Replace flag 54 | 55 | To disable the merging behavior, you can specify a `replace` boolean value for `set` like so: 56 | 57 | ```js 58 | set((state) => newState, true) 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/guides/initialize-state-with-props.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Initialize state with props 3 | nav: 13 4 | --- 5 | 6 | In cases where [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) is needed, such as when a store should be initialized with props from a component, the recommended approach is to use a vanilla store with React.context. 7 | 8 | ## Store creator with `createStore` 9 | 10 | ```ts 11 | import { createStore } from 'zustand' 12 | 13 | interface BearProps { 14 | bears: number 15 | } 16 | 17 | interface BearState extends BearProps { 18 | addBear: () => void 19 | } 20 | 21 | type BearStore = ReturnType 22 | 23 | const createBearStore = (initProps?: Partial) => { 24 | const DEFAULT_PROPS: BearProps = { 25 | bears: 0, 26 | } 27 | return createStore()((set) => ({ 28 | ...DEFAULT_PROPS, 29 | ...initProps, 30 | addBear: () => set((state) => ({ bears: ++state.bears })), 31 | })) 32 | } 33 | ``` 34 | 35 | ## Creating a context with `React.createContext` 36 | 37 | ```ts 38 | import { createContext } from 'react' 39 | 40 | export const BearContext = createContext(null) 41 | ``` 42 | 43 | ## Basic component usage 44 | 45 | ```tsx 46 | // Provider implementation 47 | import { useRef } from 'react' 48 | 49 | function App() { 50 | const store = useRef(createBearStore()).current 51 | return ( 52 | 53 | 54 | 55 | ) 56 | } 57 | ``` 58 | 59 | ```tsx 60 | // Consumer component 61 | import { useContext } from 'react' 62 | import { useStore } from 'zustand' 63 | 64 | function BasicConsumer() { 65 | const store = useContext(BearContext) 66 | if (!store) throw new Error('Missing BearContext.Provider in the tree') 67 | const bears = useStore(store, (s) => s.bears) 68 | const addBear = useStore(store, (s) => s.addBear) 69 | return ( 70 | <> 71 |
{bears} Bears.
72 | 73 | 74 | ) 75 | } 76 | ``` 77 | 78 | ## Common patterns 79 | 80 | ### Wrapping the context provider 81 | 82 | ```tsx 83 | // Provider wrapper 84 | import { useRef } from 'react' 85 | 86 | type BearProviderProps = React.PropsWithChildren 87 | 88 | function BearProvider({ children, ...props }: BearProviderProps) { 89 | const storeRef = useRef() 90 | if (!storeRef.current) { 91 | storeRef.current = createBearStore(props) 92 | } 93 | return ( 94 | 95 | {children} 96 | 97 | ) 98 | } 99 | ``` 100 | 101 | ### Extracting context logic into a custom hook 102 | 103 | ```tsx 104 | // Mimic the hook returned by `create` 105 | import { useContext } from 'react' 106 | import { useStore } from 'zustand' 107 | 108 | function useBearContext(selector: (state: BearState) => T): T { 109 | const store = useContext(BearContext) 110 | if (!store) throw new Error('Missing BearContext.Provider in the tree') 111 | return useStore(store, selector) 112 | } 113 | ``` 114 | 115 | ```tsx 116 | // Consumer usage of the custom hook 117 | function CommonConsumer() { 118 | const bears = useBearContext((s) => s.bears) 119 | const addBear = useBearContext((s) => s.addBear) 120 | return ( 121 | <> 122 |
{bears} Bears.
123 | 124 | 125 | ) 126 | } 127 | ``` 128 | 129 | ### Optionally allow using a custom equality function 130 | 131 | ```tsx 132 | // Allow custom equality function by using useStoreWithEqualityFn instead of useStore 133 | import { useContext } from 'react' 134 | import { useStoreWithEqualityFn } from 'zustand/traditional' 135 | 136 | function useBearContext( 137 | selector: (state: BearState) => T, 138 | equalityFn?: (left: T, right: T) => boolean, 139 | ): T { 140 | const store = useContext(BearContext) 141 | if (!store) throw new Error('Missing BearContext.Provider in the tree') 142 | return useStoreWithEqualityFn(store, selector, equalityFn) 143 | } 144 | ``` 145 | 146 | ### Complete example 147 | 148 | ```tsx 149 | // Provider wrapper & custom hook consumer 150 | function App2() { 151 | return ( 152 | 153 | 154 | 155 | ) 156 | } 157 | ``` 158 | -------------------------------------------------------------------------------- /docs/guides/maps-and-sets-usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Map and Set Usage 3 | nav: 10 4 | --- 5 | 6 | You need to wrap Maps and Sets inside an object. When you want its update to be reflected (e.g. in React), 7 | you do it by calling `setState` on it: 8 | 9 | **You can view a codesandbox here: https://codesandbox.io/s/late-https-bxz9qy** 10 | 11 | ```js 12 | import { create } from 'zustand' 13 | 14 | const useFooBar = create(() => ({ foo: new Map(), bar: new Set() })) 15 | 16 | function doSomething() { 17 | // doing something... 18 | 19 | // If you want to update some React component that uses `useFooBar`, you have to call setState 20 | // to let React know that an update happened. 21 | // Following React's best practices, you should create a new Map/Set when updating them: 22 | useFooBar.setState((prev) => ({ 23 | foo: new Map(prev.foo).set('newKey', 'newValue'), 24 | bar: new Set(prev.bar).add('newKey'), 25 | })) 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/guides/practice-with-no-store-actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Practice with no store actions 3 | nav: 6 4 | --- 5 | 6 | The recommended usage is to colocate actions and states within the store (let your actions be located together with your state). 7 | 8 | For example: 9 | 10 | ```js 11 | export const useBoundStore = create((set) => ({ 12 | count: 0, 13 | text: 'hello', 14 | inc: () => set((state) => ({ count: state.count + 1 })), 15 | setText: (text) => set({ text }), 16 | })) 17 | ``` 18 | 19 | This creates a self-contained store with data and actions together. 20 | 21 | --- 22 | 23 | An alternative approach is to define actions at module level, external to the store. 24 | 25 | ```js 26 | export const useBoundStore = create(() => ({ 27 | count: 0, 28 | text: 'hello', 29 | })) 30 | 31 | export const inc = () => 32 | useBoundStore.setState((state) => ({ count: state.count + 1 })) 33 | 34 | export const setText = (text) => useBoundStore.setState({ text }) 35 | ``` 36 | 37 | This has a few advantages: 38 | 39 | - It doesn't require a hook to call an action; 40 | - It facilitates code splitting. 41 | 42 | While this pattern doesn't offer any downsides, some may prefer colocating due to its encapsulated nature. 43 | -------------------------------------------------------------------------------- /docs/guides/prevent-rerenders-with-use-shallow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Prevent rerenders with useShallow 3 | nav: 15 4 | --- 5 | 6 | When you need to subscribe to a computed state from a store, the recommended way is to 7 | use a selector. 8 | 9 | The computed selector will cause a rerender if the output has changed according to [Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is?retiredLocale=it). 10 | 11 | In this case you might want to use `useShallow` to avoid a rerender if the computed value is always shallow 12 | equal the previous one. 13 | 14 | ## Example 15 | 16 | We have a store that associates to each bear a meal and we want to render their names. 17 | 18 | ```js 19 | import { create } from 'zustand' 20 | 21 | const useMeals = create(() => ({ 22 | papaBear: 'large porridge-pot', 23 | mamaBear: 'middle-size porridge pot', 24 | littleBear: 'A little, small, wee pot', 25 | })) 26 | 27 | export const BearNames = () => { 28 | const names = useMeals((state) => Object.keys(state)) 29 | 30 | return
{names.join(', ')}
31 | } 32 | ``` 33 | 34 | Now papa bear wants a pizza instead: 35 | 36 | ```js 37 | useMeals.setState({ 38 | papaBear: 'a large pizza', 39 | }) 40 | ``` 41 | 42 | This change causes `BearNames` rerenders even though the actual output of `names` has not changed according to shallow equal. 43 | 44 | We can fix that using `useShallow`! 45 | 46 | ```js 47 | import { create } from 'zustand' 48 | import { useShallow } from 'zustand/react/shallow' 49 | 50 | const useMeals = create(() => ({ 51 | papaBear: 'large porridge-pot', 52 | mamaBear: 'middle-size porridge pot', 53 | littleBear: 'A little, small, wee pot', 54 | })) 55 | 56 | export const BearNames = () => { 57 | const names = useMeals(useShallow((state) => Object.keys(state))) 58 | 59 | return
{names.join(', ')}
60 | } 61 | ``` 62 | 63 | Now they can all order other meals without causing unnecessary rerenders of our `BearNames` component. 64 | -------------------------------------------------------------------------------- /docs/guides/slices-pattern.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Slices Pattern 3 | nav: 14 4 | --- 5 | 6 | ## Slicing the store into smaller stores 7 | 8 | Your store can become bigger and bigger and tougher to maintain as you add more features. 9 | 10 | You can divide your main store into smaller individual stores to achieve modularity. This is simple to accomplish in Zustand! 11 | 12 | The first individual store: 13 | 14 | ```js 15 | export const createFishSlice = (set) => ({ 16 | fishes: 0, 17 | addFish: () => set((state) => ({ fishes: state.fishes + 1 })), 18 | }) 19 | ``` 20 | 21 | Another individual store: 22 | 23 | ```js 24 | export const createBearSlice = (set) => ({ 25 | bears: 0, 26 | addBear: () => set((state) => ({ bears: state.bears + 1 })), 27 | eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), 28 | }) 29 | ``` 30 | 31 | You can now combine both the stores into **one bounded store**: 32 | 33 | ```js 34 | import { create } from 'zustand' 35 | import { createBearSlice } from './bearSlice' 36 | import { createFishSlice } from './fishSlice' 37 | 38 | export const useBoundStore = create((...a) => ({ 39 | ...createBearSlice(...a), 40 | ...createFishSlice(...a), 41 | })) 42 | ``` 43 | 44 | ### Usage in a React component 45 | 46 | ```jsx 47 | import { useBoundStore } from './stores/useBoundStore' 48 | 49 | function App() { 50 | const bears = useBoundStore((state) => state.bears) 51 | const fishes = useBoundStore((state) => state.fishes) 52 | const addBear = useBoundStore((state) => state.addBear) 53 | return ( 54 |
55 |

Number of bears: {bears}

56 |

Number of fishes: {fishes}

57 | 58 |
59 | ) 60 | } 61 | 62 | export default App 63 | ``` 64 | 65 | ### Updating multiple stores 66 | 67 | You can update multiple stores, at the same time, in a single function. 68 | 69 | ```js 70 | export const createBearFishSlice = (set, get) => ({ 71 | addBearAndFish: () => { 72 | get().addBear() 73 | get().addFish() 74 | }, 75 | }) 76 | ``` 77 | 78 | Combining all the stores together is the same as before. 79 | 80 | ```js 81 | import { create } from 'zustand' 82 | import { createBearSlice } from './bearSlice' 83 | import { createFishSlice } from './fishSlice' 84 | import { createBearFishSlice } from './createBearFishSlice' 85 | 86 | export const useBoundStore = create((...a) => ({ 87 | ...createBearSlice(...a), 88 | ...createFishSlice(...a), 89 | ...createBearFishSlice(...a), 90 | })) 91 | ``` 92 | 93 | ## Adding middlewares 94 | 95 | Adding middlewares to a combined store is the same as with other normal stores. 96 | 97 | Adding `persist` middleware to our `useBoundStore`: 98 | 99 | ```js 100 | import { create } from 'zustand' 101 | import { createBearSlice } from './bearSlice' 102 | import { createFishSlice } from './fishSlice' 103 | import { persist } from 'zustand/middleware' 104 | 105 | export const useBoundStore = create( 106 | persist( 107 | (...a) => ({ 108 | ...createBearSlice(...a), 109 | ...createFishSlice(...a), 110 | }), 111 | { name: 'bound-store' }, 112 | ), 113 | ) 114 | ``` 115 | 116 | Please keep in mind you should only apply middlewares in the combined store. Applying them inside individual slices can lead to unexpected issues. 117 | 118 | ## Usage with TypeScript 119 | 120 | A detailed guide on how to use the slice pattern in Zustand with TypeScript can be found [here](./typescript.md#slices-pattern). 121 | -------------------------------------------------------------------------------- /docs/guides/ssr-and-hydration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SSR and Hydration 3 | nav: 16 4 | --- 5 | 6 | ## Server-side Rendering (SSR) 7 | 8 | Server-side Rendering (SSR) is a technique that helps us render our components into 9 | HTML strings on the server, send them directly to the browser, and finally "hydrate" the 10 | static markup into a fully interactive app on the client. 11 | 12 | ### React 13 | 14 | Let's say we want to render a stateless app using React. In order to do that, we need 15 | to use `express`, `react` and `react-dom/server`. We don't need `react-dom/client` 16 | since it's a stateless app. 17 | 18 | Let's dive into that: 19 | 20 | - `express` helps us build a web app that we can run using Node, 21 | - `react` helps us build the UI components that we use in our app, 22 | - `react-dom/server` helps us render our components on a server. 23 | 24 | ```json 25 | // tsconfig.json 26 | { 27 | "compilerOptions": { 28 | "noImplicitAny": false, 29 | "noEmitOnError": true, 30 | "removeComments": false, 31 | "sourceMap": true, 32 | "target": "esnext" 33 | }, 34 | "include": ["**/*"] 35 | } 36 | ``` 37 | 38 | > **Note:** do not forget to remove all comments from your `tsconfig.json` file. 39 | 40 | ```tsx 41 | // app.tsx 42 | export const App = () => { 43 | return ( 44 | 45 | 46 | 47 | 48 | Static Server-side-rendered App 49 | 50 | 51 |
Hello World!
52 | 53 | 54 | ) 55 | } 56 | ``` 57 | 58 | ```tsx 59 | // server.tsx 60 | import express from 'express' 61 | import React from 'react' 62 | import ReactDOMServer from 'react-dom/server' 63 | 64 | import { App } from './app.tsx' 65 | 66 | const port = Number.parseInt(process.env.PORT || '3000', 10) 67 | const app = express() 68 | 69 | app.get('/', (_, res) => { 70 | const { pipe } = ReactDOMServer.renderToPipeableStream(, { 71 | onShellReady() { 72 | res.setHeader('content-type', 'text/html') 73 | pipe(res) 74 | }, 75 | }) 76 | }) 77 | 78 | app.listen(port, () => { 79 | console.log(`Server is listening at ${port}`) 80 | }) 81 | ``` 82 | 83 | ```sh 84 | tsc --build 85 | ``` 86 | 87 | ```sh 88 | node server.js 89 | ``` 90 | 91 | ## Hydration 92 | 93 | Hydration turns the initial HTML snapshot from the server into a fully interactive app 94 | that runs in the browser. The right way to "hydrate" a component is by using `hydrateRoot`. 95 | 96 | ### React 97 | 98 | Let's say we want to render a stateful app using React. In order to do that we need to 99 | use `express`, `react`, `react-dom/server` and `react-dom/client`. 100 | 101 | Let's dive into that: 102 | 103 | - `express` helps us build a web app that we can run using Node, 104 | - `react` helps us build the UI components that we use in our app, 105 | - `react-dom/server` helps us render our components on a server, 106 | - `react-dom/client` helps us hydrate our components on a client. 107 | 108 | > **Note:** Do not forget that even if we can render our components on a server, it is 109 | > important to "hydrate" them on a client to make them interactive. 110 | 111 | ```json 112 | // tsconfig.json 113 | { 114 | "compilerOptions": { 115 | "noImplicitAny": false, 116 | "noEmitOnError": true, 117 | "removeComments": false, 118 | "sourceMap": true, 119 | "target": "esnext" 120 | }, 121 | "include": ["**/*"] 122 | } 123 | ``` 124 | 125 | > **Note:** do not forget to remove all comments in your `tsconfig.json` file. 126 | 127 | ```tsx 128 | // app.tsx 129 | export const App = () => { 130 | return ( 131 | 132 | 133 | 134 | 135 | Static Server-side-rendered App 136 | 137 | 138 |
Hello World!
139 | 140 | 141 | ) 142 | } 143 | ``` 144 | 145 | ```tsx 146 | // main.tsx 147 | import ReactDOMClient from 'react-dom/client' 148 | 149 | import { App } from './app.tsx' 150 | 151 | ReactDOMClient.hydrateRoot(document, ) 152 | ``` 153 | 154 | ```tsx 155 | // server.tsx 156 | import express from 'express' 157 | import React from 'react' 158 | import ReactDOMServer from 'react-dom/server' 159 | 160 | import { App } from './app.tsx' 161 | 162 | const port = Number.parseInt(process.env.PORT || '3000', 10) 163 | const app = express() 164 | 165 | app.use('/', (_, res) => { 166 | const { pipe } = ReactDOMServer.renderToPipeableStream(, { 167 | bootstrapScripts: ['/main.js'], 168 | onShellReady() { 169 | res.setHeader('content-type', 'text/html') 170 | pipe(res) 171 | }, 172 | }) 173 | }) 174 | 175 | app.listen(port, () => { 176 | console.log(`Server is listening at ${port}`) 177 | }) 178 | ``` 179 | 180 | ```sh 181 | tsc --build 182 | ``` 183 | 184 | ```sh 185 | node server.js 186 | ``` 187 | 188 | > **Warning:** The React tree you pass to `hydrateRoot` needs to produce the same output as it did on the server. 189 | > The most common causes leading to hydration errors include: 190 | > 191 | > - Extra whitespace (like newlines) around the React-generated HTML inside the root node. 192 | > - Using checks like typeof window !== 'undefined' in your rendering logic. 193 | > - Using browser-only APIs like `window.matchMedia` in your rendering logic. 194 | > - Rendering different data on the server and the client. 195 | > 196 | > React recovers from some hydration errors, but you must fix them like other bugs. In the best case, they’ll lead to a slowdown; in the worst case, event handlers can get attached to the wrong elements. 197 | 198 | You can read more about the caveats and pitfalls here: [hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot) 199 | -------------------------------------------------------------------------------- /docs/guides/updating-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Updating state 3 | nav: 2 4 | --- 5 | 6 | ## Flat updates 7 | 8 | Updating state with Zustand is simple! Call the provided `set` function with 9 | the new state, and it will be shallowly merged with the existing state in the 10 | store. **Note** See next section for nested state. 11 | 12 | ```tsx 13 | import { create } from 'zustand' 14 | 15 | type State = { 16 | firstName: string 17 | lastName: string 18 | } 19 | 20 | type Action = { 21 | updateFirstName: (firstName: State['firstName']) => void 22 | updateLastName: (lastName: State['lastName']) => void 23 | } 24 | 25 | // Create your store, which includes both state and (optionally) actions 26 | const usePersonStore = create((set) => ({ 27 | firstName: '', 28 | lastName: '', 29 | updateFirstName: (firstName) => set(() => ({ firstName: firstName })), 30 | updateLastName: (lastName) => set(() => ({ lastName: lastName })), 31 | })) 32 | 33 | // In consuming app 34 | function App() { 35 | // "select" the needed state and actions, in this case, the firstName value 36 | // and the action updateFirstName 37 | const firstName = usePersonStore((state) => state.firstName) 38 | const updateFirstName = usePersonStore((state) => state.updateFirstName) 39 | 40 | return ( 41 |
42 | 50 | 51 |

52 | Hello, {firstName}! 53 |

54 |
55 | ) 56 | } 57 | ``` 58 | 59 | ## Deeply nested object 60 | 61 | If you have a deep state object like this: 62 | 63 | ```ts 64 | type State = { 65 | deep: { 66 | nested: { 67 | obj: { count: number } 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | Updating nested state requires some effort to ensure the process is completed 74 | immutably. 75 | 76 | ### Normal approach 77 | 78 | Similar to React or Redux, the normal approach is to copy each level of the 79 | state object. This is done with the spread operator `...`, and by manually 80 | merging that in with the new state values. Like so: 81 | 82 | ```ts 83 | normalInc: () => 84 | set((state) => ({ 85 | deep: { 86 | ...state.deep, 87 | nested: { 88 | ...state.deep.nested, 89 | obj: { 90 | ...state.deep.nested.obj, 91 | count: state.deep.nested.obj.count + 1 92 | } 93 | } 94 | } 95 | })), 96 | ``` 97 | 98 | This is very long! Let's explore some alternatives that will make your life 99 | easier. 100 | 101 | ### With Immer 102 | 103 | Many people use [Immer](https://github.com/immerjs/immer) to update nested 104 | values. Immer can be used anytime you need to update nested state such as in 105 | React, Redux and of course, Zustand! 106 | 107 | You can use Immer to shorten your state updates for deeply nested object. Let's 108 | take a look at an example: 109 | 110 | ```ts 111 | immerInc: () => 112 | set(produce((state: State) => { ++state.deep.nested.obj.count })), 113 | ``` 114 | 115 | What a reduction! Please take note of the [gotchas listed here](../integrations/immer-middleware.md). 116 | 117 | ### With optics-ts 118 | 119 | There is another option with [optics-ts](https://github.com/akheron/optics-ts/): 120 | 121 | ```ts 122 | opticsInc: () => 123 | set(O.modify(O.optic().path("deep.nested.obj.count"))((c) => c + 1)), 124 | ``` 125 | 126 | Unlike Immer, optics-ts doesn't use proxies or mutation syntax. 127 | 128 | ### With Ramda 129 | 130 | You can also use [Ramda](https://ramdajs.com/): 131 | 132 | ```ts 133 | ramdaInc: () => 134 | set(R.modifyPath(["deep", "nested", "obj", "count"], (c) => c + 1)), 135 | ``` 136 | 137 | Both ramda and optics-ts also work with types. 138 | 139 | ### CodeSandbox Demo 140 | 141 | https://codesandbox.io/s/zustand-normal-immer-optics-ramda-updating-ynn3o?file=/src/App.tsx 142 | -------------------------------------------------------------------------------- /docs/hooks/use-shallow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useShallow ⚛️ 3 | description: How to memoize selector functions 4 | nav: 28 5 | --- 6 | 7 | `useShallow` is a React Hook that lets you optimize re-renders. 8 | 9 | ```js 10 | const memoizedSelector = useShallow(selector) 11 | ``` 12 | 13 | - [Types](#types) 14 | - [Signature](#signature) 15 | - [Reference](#reference) 16 | - [Usage](#usage) 17 | - [Writing a memoized selector](#writing-a-memoized-selector) 18 | - [Troubleshooting](#troubleshooting) 19 | 20 | ### Signature 21 | 22 | ```ts 23 | useShallow(selectorFn: (state: T) => U): (state: T) => U 24 | ``` 25 | 26 | ## Reference 27 | 28 | ### `useShallow(selectorFn)` 29 | 30 | #### Parameters 31 | 32 | - `selectorFn`: A function that lets you return data that is based on current state. 33 | 34 | #### Returns 35 | 36 | `useShallow` returns a memoized version of a selector function using a shallow comparison for 37 | memoization. 38 | 39 | ## Usage 40 | 41 | ### Writing a memoized selector 42 | 43 | First, we need to setup a store to hold the state for the bear family. In this store, we define 44 | three properties: `papaBear`, `mamaBear`, and `babyBear`, each representing a different member of 45 | the bear family and their respective oatmeal pot sizes. 46 | 47 | ```tsx 48 | import { create } from 'zustand' 49 | 50 | type BearFamilyMealsStore = { 51 | [key: string]: string 52 | } 53 | 54 | const useBearFamilyMealsStore = create()(() => ({ 55 | papaBear: 'large porridge-pot', 56 | mamaBear: 'middle-size porridge pot', 57 | babyBear: 'A little, small, wee pot', 58 | })) 59 | ``` 60 | 61 | Next, we'll create a `BearNames` component that retrieves the keys of our state (the bear family 62 | members) and displays them. 63 | 64 | ```tsx 65 | function BearNames() { 66 | const names = useBearFamilyMealsStore((state) => Object.keys(state)) 67 | 68 | return
{names.join(', ')}
69 | } 70 | ``` 71 | 72 | Next, we will create a `UpdateBabyBearMeal` component that periodically updates baby bear's meal 73 | choice. 74 | 75 | ```tsx 76 | const meals = [ 77 | 'A tiny, little, wee bowl', 78 | 'A small, petite, tiny pot', 79 | 'A wee, itty-bitty, small bowl', 80 | 'A little, petite, tiny dish', 81 | 'A tiny, small, wee vessel', 82 | 'A small, little, wee cauldron', 83 | 'A little, tiny, small cup', 84 | 'A wee, small, little jar', 85 | 'A tiny, wee, small pan', 86 | 'A small, wee, little crock', 87 | ] 88 | 89 | function UpdateBabyBearMeal() { 90 | useEffect(() => { 91 | const timer = setInterval(() => { 92 | useBearFamilyMealsStore.setState({ 93 | babyBear: meals[Math.floor(Math.random() * (meals.length - 1))], 94 | }) 95 | }, 1000) 96 | 97 | return () => { 98 | clearInterval(timer) 99 | } 100 | }, []) 101 | 102 | return null 103 | } 104 | ``` 105 | 106 | Finally, we combine both components in the `App` component to see them in action. 107 | 108 | ```tsx 109 | export default function App() { 110 | return ( 111 | <> 112 | 113 | 114 | 115 | ) 116 | } 117 | ``` 118 | 119 | Here is what the code should look like: 120 | 121 | ```tsx 122 | import { useEffect } from 'react' 123 | import { create } from 'zustand' 124 | 125 | type BearFamilyMealsStore = { 126 | [key: string]: string 127 | } 128 | 129 | const useBearFamilyMealsStore = create()(() => ({ 130 | papaBear: 'large porridge-pot', 131 | mamaBear: 'middle-size porridge pot', 132 | babyBear: 'A little, small, wee pot', 133 | })) 134 | 135 | const meals = [ 136 | 'A tiny, little, wee bowl', 137 | 'A small, petite, tiny pot', 138 | 'A wee, itty-bitty, small bowl', 139 | 'A little, petite, tiny dish', 140 | 'A tiny, small, wee vessel', 141 | 'A small, little, wee cauldron', 142 | 'A little, tiny, small cup', 143 | 'A wee, small, little jar', 144 | 'A tiny, wee, small pan', 145 | 'A small, wee, little crock', 146 | ] 147 | 148 | function UpdateBabyBearMeal() { 149 | useEffect(() => { 150 | const timer = setInterval(() => { 151 | useBearFamilyMealsStore.setState({ 152 | babyBear: meals[Math.floor(Math.random() * (meals.length - 1))], 153 | }) 154 | }, 1000) 155 | 156 | return () => { 157 | clearInterval(timer) 158 | } 159 | }, []) 160 | 161 | return null 162 | } 163 | 164 | function BearNames() { 165 | const names = useBearFamilyMealsStore((state) => Object.keys(state)) 166 | 167 | return
{names.join(', ')}
168 | } 169 | 170 | export default function App() { 171 | return ( 172 | <> 173 | 174 | 175 | 176 | ) 177 | } 178 | ``` 179 | 180 | Everything might look fine, but there’s a small problem: the `BearNames` component keeps 181 | re-rendering even if the names haven’t changed. This happens because the component re-renders 182 | whenever any part of the state changes, even if the specific part we care about (the list of names) hasn’t changed. 183 | 184 | To fix this, we use `useShallow` to make sure the component only re-renders when the actual keys of 185 | the state change: 186 | 187 | ```tsx 188 | function BearNames() { 189 | const names = useBearFamilyMealsStore( 190 | useShallow((state) => Object.keys(state)), 191 | ) 192 | 193 | return
{names.join(', ')}
194 | } 195 | ``` 196 | 197 | Here is what the code should look like: 198 | 199 | ```tsx 200 | import { useEffect } from 'react' 201 | import { create } from 'zustand' 202 | import { useShallow } from 'zustand/react/shallow' 203 | 204 | type BearFamilyMealsStore = { 205 | [key: string]: string 206 | } 207 | 208 | const useBearFamilyMealsStore = create()(() => ({ 209 | papaBear: 'large porridge-pot', 210 | mamaBear: 'middle-size porridge pot', 211 | babyBear: 'A little, small, wee pot', 212 | })) 213 | 214 | const meals = [ 215 | 'A tiny, little, wee bowl', 216 | 'A small, petite, tiny pot', 217 | 'A wee, itty-bitty, small bowl', 218 | 'A little, petite, tiny dish', 219 | 'A tiny, small, wee vessel', 220 | 'A small, little, wee cauldron', 221 | 'A little, tiny, small cup', 222 | 'A wee, small, little jar', 223 | 'A tiny, wee, small pan', 224 | 'A small, wee, little crock', 225 | ] 226 | 227 | function UpdateBabyBearMeal() { 228 | useEffect(() => { 229 | const timer = setInterval(() => { 230 | useBearFamilyMealsStore.setState({ 231 | babyBear: meals[Math.floor(Math.random() * (meals.length - 1))], 232 | }) 233 | }, 1000) 234 | 235 | return () => { 236 | clearInterval(timer) 237 | } 238 | }, []) 239 | 240 | return null 241 | } 242 | 243 | function BearNames() { 244 | const names = useBearFamilyMealsStore( 245 | useShallow((state) => Object.keys(state)), 246 | ) 247 | 248 | return
{names.join(', ')}
249 | } 250 | 251 | export default function App() { 252 | return ( 253 | <> 254 | 255 | 256 | 257 | ) 258 | } 259 | ``` 260 | 261 | By using `useShallow`, we optimized the rendering process, ensuring that the component only 262 | re-renders when necessary, which improves overall performance. 263 | 264 | ## Troubleshooting 265 | 266 | TBD 267 | -------------------------------------------------------------------------------- /docs/integrations/immer-middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Immer middleware 3 | nav: 18 4 | --- 5 | 6 | The [Immer](https://github.com/immerjs/immer) middleware enables you 7 | to use immutable state in a more convenient way. 8 | Also, with Immer, you can simplify handling 9 | immutable data structures in Zustand. 10 | 11 | ## Installation 12 | 13 | In order to use the Immer middleware in Zustand, 14 | you will need to install Immer as a direct dependency. 15 | 16 | ```bash 17 | npm install immer 18 | ``` 19 | 20 | ## Usage 21 | 22 | (Notice the extra parentheses after the type parameter as mentioned in the [Typescript Guide](../guides/typescript.md)). 23 | 24 | Updating simple states 25 | 26 | ```ts 27 | import { create } from 'zustand' 28 | import { immer } from 'zustand/middleware/immer' 29 | 30 | type State = { 31 | count: number 32 | } 33 | 34 | type Actions = { 35 | increment: (qty: number) => void 36 | decrement: (qty: number) => void 37 | } 38 | 39 | export const useCountStore = create()( 40 | immer((set) => ({ 41 | count: 0, 42 | increment: (qty: number) => 43 | set((state) => { 44 | state.count += qty 45 | }), 46 | decrement: (qty: number) => 47 | set((state) => { 48 | state.count -= qty 49 | }), 50 | })), 51 | ) 52 | ``` 53 | 54 | Updating complex states 55 | 56 | ```ts 57 | import { create } from 'zustand' 58 | import { immer } from 'zustand/middleware/immer' 59 | 60 | interface Todo { 61 | id: string 62 | title: string 63 | done: boolean 64 | } 65 | 66 | type State = { 67 | todos: Record 68 | } 69 | 70 | type Actions = { 71 | toggleTodo: (todoId: string) => void 72 | } 73 | 74 | export const useTodoStore = create()( 75 | immer((set) => ({ 76 | todos: { 77 | '82471c5f-4207-4b1d-abcb-b98547e01a3e': { 78 | id: '82471c5f-4207-4b1d-abcb-b98547e01a3e', 79 | title: 'Learn Zustand', 80 | done: false, 81 | }, 82 | '354ee16c-bfdd-44d3-afa9-e93679bda367': { 83 | id: '354ee16c-bfdd-44d3-afa9-e93679bda367', 84 | title: 'Learn Jotai', 85 | done: false, 86 | }, 87 | '771c85c5-46ea-4a11-8fed-36cc2c7be344': { 88 | id: '771c85c5-46ea-4a11-8fed-36cc2c7be344', 89 | title: 'Learn Valtio', 90 | done: false, 91 | }, 92 | '363a4bac-083f-47f7-a0a2-aeeee153a99c': { 93 | id: '363a4bac-083f-47f7-a0a2-aeeee153a99c', 94 | title: 'Learn Signals', 95 | done: false, 96 | }, 97 | }, 98 | toggleTodo: (todoId: string) => 99 | set((state) => { 100 | state.todos[todoId].done = !state.todos[todoId].done 101 | }), 102 | })), 103 | ) 104 | ``` 105 | 106 | ## Gotchas 107 | 108 | In this section you will find some things 109 | that you need to keep in mind when using Zustand with Immer. 110 | 111 | ### My subscriptions aren't being called 112 | 113 | If you are using Immer, 114 | make sure you are actually following 115 | [the rules of Immer](https://immerjs.github.io/immer/pitfalls). 116 | 117 | For example, you have to add `[immerable] = true` for 118 | [class objects](https://immerjs.github.io/immer/complex-objects) to work. 119 | If you don't do this, Immer will still mutate the object, 120 | but not as a proxy, so it will also update the current state. 121 | Zustand checks if the state has actually changed, 122 | so since both the current state and the next state are 123 | equal (if you don't do it correctly), 124 | Zustand will skip calling the subscriptions. 125 | 126 | ## CodeSandbox Demo 127 | 128 | - [Basic](https://codesandbox.io/p/sandbox/zustand-updating-draft-states-basic-demo-forked-96mkdw), 129 | - [Advanced](https://codesandbox.io/p/sandbox/zustand-updating-draft-states-advanced-demo-forked-phkzzg). 130 | -------------------------------------------------------------------------------- /docs/middlewares/combine.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: combine 3 | description: How to create a store and get types automatically inferred 4 | nav: 201 5 | --- 6 | 7 | # combine 8 | 9 | `combine` middleware lets you create a cohesive state by merging an initial state with a state 10 | creator function that adds new state slices and actions. This is really helpful as it automatically 11 | infers types, so there’s no need for explicit type definitions. 12 | 13 | > [!TIP] 14 | > This makes state management more straightforward and efficient by making curried version of 15 | > `create` and `createStore` not necessary for middleware usage. 16 | 17 | ```js 18 | const nextStateCreatorFn = combine(initialState, additionalStateCreatorFn) 19 | ``` 20 | 21 | - [Types](#types) 22 | - [Signature](#combine-signature) 23 | - [Reference](#reference) 24 | - [Usage](#usage) 25 | - [Creating a state with inferred types](#creating-a-state-wit-inferred-types) 26 | - [Troubleshooting](#troubleshooting) 27 | 28 | ## Types 29 | 30 | ### Signature 31 | 32 | ```ts 33 | combine(initialState: T, additionalStateCreatorFn: StateCreator): StateCreator & U, [], []> 34 | ``` 35 | 36 | ## Reference 37 | 38 | ### `combine(initialState, additionalStateCreatorFn)` 39 | 40 | #### Parameters 41 | 42 | - `initialState`: The value you want the state to be initially. It can be a value of any type, 43 | except a function. 44 | - `additionalStateCreatorFn`: A function that takes `set` function, `get` function and `store` as 45 | arguments. Usually, you will return an object with the methods you want to expose. 46 | 47 | #### Returns 48 | 49 | `combine` returns a state creator function. 50 | 51 | ## Usage 52 | 53 | ### Creating a store with inferred types 54 | 55 | This example shows you how you can create a store and get types automatically inferred, so you 56 | don’t need to define them explicitly. 57 | 58 | ```ts 59 | import { createStore } from 'zustand/vanilla' 60 | import { combine } from 'zustand/middleware' 61 | 62 | const positionStore = createStore( 63 | combine({ position: { x: 0, y: 0 } }, (set) => ({ 64 | setPosition: (position) => set({ position }), 65 | })), 66 | ) 67 | 68 | const $dotContainer = document.getElementById('dot-container') as HTMLDivElement 69 | const $dot = document.getElementById('dot') as HTMLDivElement 70 | 71 | $dotContainer.addEventListener('pointermove', (event) => { 72 | positionStore.getState().setPosition({ 73 | x: event.clientX, 74 | y: event.clientY, 75 | }) 76 | }) 77 | 78 | const render: Parameters[0] = (state) => { 79 | $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` 80 | } 81 | 82 | render(positionStore.getInitialState(), positionStore.getInitialState()) 83 | 84 | positionStore.subscribe(render) 85 | ``` 86 | 87 | Here's the `html` code 88 | 89 | ```html 90 |
94 |
98 |
99 | ``` 100 | 101 | ## Troubleshooting 102 | 103 | TBD 104 | -------------------------------------------------------------------------------- /docs/middlewares/devtools.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: devtools 3 | description: How to time-travel debug your store 4 | nav: 205 5 | --- 6 | 7 | # devtools 8 | 9 | `devtools` middleware lets you use [Redux DevTools Extension](https://github.com/reduxjs/redux-devtools) 10 | without Redux. Read more about the benefits of using [Redux DevTools for debugging](https://redux.js.org/style-guide/#use-the-redux-devtools-extension-for-debugging). 11 | 12 | ```js 13 | const nextStateCreatorFn = devtools(stateCreatorFn, devtoolsOptions) 14 | ``` 15 | 16 | - [Types](#types) 17 | - [Signature](#signature) 18 | - [Mutator](#mutator) 19 | - [Reference](#reference) 20 | - [Usage](#usage) 21 | - [Debugging a store](#debugging-a-store) 22 | - [Debugging a Slices pattern based store](#debugging-a-slices-pattern-based-store) 23 | - [Troubleshooting](#troubleshooting) 24 | - [Only one store is displayed](#only-one-store-is-displayed) 25 | - [Action names are labeled as 'anonymous'](#all-action-names-are-labeled-as-anonymous) 26 | 27 | ## Types 28 | 29 | ### Signature 30 | 31 | ```ts 32 | devtools(stateCreatorFn: StateCreator, devtoolsOptions?: DevtoolsOptions): StateCreator 33 | ``` 34 | 35 | ### Mutator 36 | 37 | 38 | ```ts 39 | ['zustand/devtools', never] 40 | ``` 41 | 42 | 43 | ## Reference 44 | 45 | ### `devtools(stateCreatorFn, devtoolsOptions)` 46 | 47 | #### Parameters 48 | 49 | - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. 50 | Usually, you will return an object with the methods you want to expose. 51 | - **optional** `devtoolsOptions`: An object to define `Redux Devtools` options. 52 | - **optional** `name`: A custom identifier for the connection in the Redux DevTools. 53 | - **optional** `enabled`: Defaults to `true` when is on development mode, and defaults to `false` 54 | when is on production mode. Enables or disables the Redux DevTools integration 55 | for this store. 56 | - **optional** `anonymousActionType`: Defaults to `anonymous`. A string to use as the action type 57 | for anonymous mutations in the Redux DevTools. 58 | - **optional** `store`: A custom identifier for the store in the Redux DevTools. 59 | 60 | #### Returns 61 | 62 | `devtools` returns a state creator function. 63 | 64 | ## Usage 65 | 66 | ### Debugging a store 67 | 68 | This example shows you how you can use `Redux Devtools` to debug a store 69 | 70 | ```ts 71 | import { create, StateCreator } from 'zustand' 72 | import { devtools } from 'zustand/middleware' 73 | 74 | type JungleStore = { 75 | bears: number 76 | addBear: () => void 77 | fishes: number 78 | addFish: () => void 79 | } 80 | 81 | const useJungleStore = create()( 82 | devtools((set) => ({ 83 | bears: 0, 84 | addBear: () => 85 | set((state) => ({ bears: state.bears + 1 }), undefined, 'jungle/addBear'), 86 | fishes: 0, 87 | addFish: () => 88 | set( 89 | (state) => ({ fishes: state.fishes + 1 }), 90 | undefined, 91 | 'jungle/addFish', 92 | ), 93 | })), 94 | ) 95 | ``` 96 | 97 | ### Debugging a Slices pattern based store 98 | 99 | This example shows you how you can use `Redux Devtools` to debug a Slices pattern based store 100 | 101 | ```ts 102 | import { create, StateCreator } from 'zustand' 103 | import { devtools } from 'zustand/middleware' 104 | 105 | type BearSlice = { 106 | bears: number 107 | addBear: () => void 108 | } 109 | 110 | type FishSlice = { 111 | fishes: number 112 | addFish: () => void 113 | } 114 | 115 | type JungleStore = BearSlice & FishSlice 116 | 117 | const createBearSlice: StateCreator< 118 | JungleStore, 119 | [['zustand/devtools', never]], 120 | [], 121 | BearSlice 122 | > = (set) => ({ 123 | bears: 0, 124 | addBear: () => 125 | set( 126 | (state) => ({ bears: state.bears + 1 }), 127 | undefined, 128 | 'jungle:bear/addBear', 129 | ), 130 | }) 131 | 132 | const createFishSlice: StateCreator< 133 | JungleStore, 134 | [['zustand/devtools', never]], 135 | [], 136 | FishSlice 137 | > = (set) => ({ 138 | fishes: 0, 139 | addFish: () => 140 | set( 141 | (state) => ({ fishes: state.fishes + 1 }), 142 | undefined, 143 | 'jungle:fish/addFish', 144 | ), 145 | }) 146 | 147 | const useJungleStore = create()( 148 | devtools((...args) => ({ 149 | ...createBearSlice(...args), 150 | ...createFishSlice(...args), 151 | })), 152 | ) 153 | ``` 154 | 155 | ## Troubleshooting 156 | 157 | ### Only one store is displayed 158 | 159 | By default, `Redux Devtools` only show one store at a time, so in order to see other stores you 160 | need to use store selector and choose a different store. 161 | 162 | ### All action names are labeled as 'anonymous' 163 | 164 | If an action type name is not provided, it is defaulted to "anonymous". You can customize this 165 | default value by providing a `anonymousActionType` parameter: 166 | 167 | For instance the next example doesn't have action type name: 168 | 169 | ```ts 170 | import { create, StateCreator } from 'zustand' 171 | import { devtools } from 'zustand/middleware' 172 | 173 | type BearSlice = { 174 | bears: number 175 | addBear: () => void 176 | } 177 | 178 | type FishSlice = { 179 | fishes: number 180 | addFish: () => void 181 | } 182 | 183 | type JungleStore = BearSlice & FishSlice 184 | 185 | const createBearSlice: StateCreator< 186 | JungleStore, 187 | [['zustand/devtools', never]], 188 | [], 189 | BearSlice 190 | > = (set) => ({ 191 | bears: 0, 192 | addBear: () => set((state) => ({ bears: state.bears + 1 })), 193 | eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), 194 | }) 195 | 196 | const createFishSlice: StateCreator< 197 | JungleStore, 198 | [['zustand/devtools', never]], 199 | [], 200 | FishSlice 201 | > = (set) => ({ 202 | fishes: 0, 203 | addFish: () => set((state) => ({ fishes: state.fishes + 1 })), 204 | }) 205 | 206 | const useJungleStore = create()( 207 | devtools((...args) => ({ 208 | ...createBearSlice(...args), 209 | ...createFishSlice(...args), 210 | })), 211 | ) 212 | ``` 213 | 214 | In order to fix the previous example, we need to provide an action type name as the third parameter. 215 | Additionally, to preserve the default behavior of the replacement logic, the second parameter 216 | should be set to `undefined`. 217 | 218 | Here's the fixed previous example 219 | 220 | ```ts 221 | import { create, StateCreator } from 'zustand' 222 | 223 | type BearSlice = { 224 | bears: number 225 | addBear: () => void 226 | } 227 | 228 | type FishSlice = { 229 | fishes: number 230 | addFish: () => void 231 | } 232 | 233 | type JungleStore = BearSlice & FishSlice 234 | 235 | const createBearSlice: StateCreator< 236 | JungleStore, 237 | [['zustand/devtools', never]], 238 | [], 239 | BearSlice 240 | > = (set) => ({ 241 | bears: 0, 242 | addBear: () => 243 | set((state) => ({ bears: state.bears + 1 }), undefined, 'bear/addBear'), 244 | }) 245 | 246 | const createFishSlice: StateCreator< 247 | JungleStore, 248 | [['zustand/devtools', never]], 249 | [], 250 | FishSlice 251 | > = (set) => ({ 252 | fishes: 0, 253 | addFish: () => 254 | set((state) => ({ fishes: state.fishes + 1 }), undefined, 'fish/addFish'), 255 | }) 256 | 257 | const useJungleStore = create()( 258 | devtools((...args) => ({ 259 | ...createBearSlice(...args), 260 | ...createFishSlice(...args), 261 | })), 262 | ) 263 | ``` 264 | 265 | > [!IMPORTANT] 266 | > Do not set the second parameter to `true` or `false` unless you want to override the default 267 | > replacement logic 268 | -------------------------------------------------------------------------------- /docs/middlewares/immer.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: immer 3 | description: How to perform immutable updates in a store without boilerplate code 4 | nav: 206 5 | --- 6 | 7 | # immer 8 | 9 | `immer` middleware lets you perform immutable updates. 10 | 11 | ```js 12 | const nextStateCreatorFn = immer(stateCreatorFn) 13 | ``` 14 | 15 | - [Types](#types) 16 | - [Signature](#signature) 17 | - [Mutator](#mutator) 18 | - [Reference](#reference) 19 | - [Usage](#usage) 20 | - [Troubleshooting](#troubleshooting) 21 | 22 | ## Types 23 | 24 | ### Signature 25 | 26 | ```ts 27 | immer(stateCreatorFn: StateCreator): StateCreator 28 | ``` 29 | 30 | ### Mutator 31 | 32 | 33 | ```ts 34 | ['zustand/immer', never] 35 | ``` 36 | 37 | 38 | ## Reference 39 | 40 | ### `immer(stateCreatorFn)` 41 | 42 | #### Parameters 43 | 44 | - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. 45 | Usually, you will return an object with the methods you want to expose. 46 | 47 | #### Returns 48 | 49 | `immer` returns a state creator function. 50 | 51 | ## Usage 52 | 53 | ### Updating state without boilerplate code 54 | 55 | In the next example, we're going to update the `person` object. Since it's a nested object, we need 56 | to create a copy of the entire object before making the update. 57 | 58 | ```ts 59 | import { createStore } from 'zustand/vanilla' 60 | 61 | type PersonStoreState = { 62 | person: { firstName: string; lastName: string; email: string } 63 | } 64 | 65 | type PersonStoreActions = { 66 | setPerson: ( 67 | nextPerson: ( 68 | person: PersonStoreState['person'], 69 | ) => PersonStoreState['person'] | PersonStoreState['person'], 70 | ) => void 71 | } 72 | 73 | type PersonStore = PersonStoreState & PersonStoreActions 74 | 75 | const personStore = createStore()((set) => ({ 76 | person: { 77 | firstName: 'Barbara', 78 | lastName: 'Hepworth', 79 | email: 'bhepworth@sculpture.com', 80 | }, 81 | setPerson: (nextPerson) => 82 | set((state) => ({ 83 | person: 84 | typeof nextPerson === 'function' 85 | ? nextPerson(state.person) 86 | : nextPerson, 87 | })), 88 | })) 89 | 90 | const $firstNameInput = document.getElementById( 91 | 'first-name', 92 | ) as HTMLInputElement 93 | const $lastNameInput = document.getElementById('last-name') as HTMLInputElement 94 | const $emailInput = document.getElementById('email') as HTMLInputElement 95 | const $result = document.getElementById('result') as HTMLDivElement 96 | 97 | function handleFirstNameChange(event: Event) { 98 | personStore.getState().setPerson((person) => ({ 99 | ...person, 100 | firstName: (event.target as any).value, 101 | })) 102 | } 103 | 104 | function handleLastNameChange(event: Event) { 105 | personStore.getState().setPerson((person) => ({ 106 | ...person, 107 | lastName: (event.target as any).value, 108 | })) 109 | } 110 | 111 | function handleEmailChange(event: Event) { 112 | personStore.getState().setPerson((person) => ({ 113 | ...person, 114 | email: (event.target as any).value, 115 | })) 116 | } 117 | 118 | $firstNameInput.addEventListener('input', handleFirstNameChange) 119 | $lastNameInput.addEventListener('input', handleLastNameChange) 120 | $emailInput.addEventListener('input', handleEmailChange) 121 | 122 | const render: Parameters[0] = (state) => { 123 | $firstNameInput.value = state.person.firstName 124 | $lastNameInput.value = state.person.lastName 125 | $emailInput.value = state.person.email 126 | 127 | $result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})` 128 | } 129 | 130 | render(personStore.getInitialState(), personStore.getInitialState()) 131 | 132 | personStore.subscribe(render) 133 | ``` 134 | 135 | Here's the `html` code 136 | 137 | ```html 138 | 142 | 146 | 150 |

151 | ``` 152 | 153 | To avoid manually copying the entire object before making updates, we'll use the `immer` 154 | middleware. 155 | 156 | ```ts 157 | import { createStore } from 'zustand/vanilla' 158 | import { immer } from 'zustand/middleware/immer' 159 | 160 | type PersonStoreState = { 161 | person: { firstName: string; lastName: string; email: string } 162 | } 163 | 164 | type PersonStoreActions = { 165 | setPerson: ( 166 | nextPerson: ( 167 | person: PersonStoreState['person'], 168 | ) => PersonStoreState['person'] | PersonStoreState['person'], 169 | ) => void 170 | } 171 | 172 | type PersonStore = PersonStoreState & PersonStoreActions 173 | 174 | const personStore = createStore()( 175 | immer((set) => ({ 176 | person: { 177 | firstName: 'Barbara', 178 | lastName: 'Hepworth', 179 | email: 'bhepworth@sculpture.com', 180 | }, 181 | setPerson: (nextPerson) => 182 | set((state) => { 183 | state.person = 184 | typeof nextPerson === 'function' 185 | ? nextPerson(state.person) 186 | : nextPerson 187 | }), 188 | })), 189 | ) 190 | 191 | const $firstNameInput = document.getElementById( 192 | 'first-name', 193 | ) as HTMLInputElement 194 | const $lastNameInput = document.getElementById('last-name') as HTMLInputElement 195 | const $emailInput = document.getElementById('email') as HTMLInputElement 196 | const $result = document.getElementById('result') as HTMLDivElement 197 | 198 | function handleFirstNameChange(event: Event) { 199 | personStore.getState().setPerson((person) => { 200 | person.firstName = (event.target as any).value 201 | }) 202 | } 203 | 204 | function handleLastNameChange(event: Event) { 205 | personStore.getState().setPerson((person) => { 206 | person.lastName = (event.target as any).value 207 | }) 208 | } 209 | 210 | function handleEmailChange(event: Event) { 211 | personStore.getState().setPerson((person) => { 212 | person.email = (event.target as any).value 213 | }) 214 | } 215 | 216 | $firstNameInput.addEventListener('input', handleFirstNameChange) 217 | $lastNameInput.addEventListener('input', handleLastNameChange) 218 | $emailInput.addEventListener('input', handleEmailChange) 219 | 220 | const render: Parameters[0] = (state) => { 221 | $firstNameInput.value = state.person.firstName 222 | $lastNameInput.value = state.person.lastName 223 | $emailInput.value = state.person.email 224 | 225 | $result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})` 226 | } 227 | 228 | render(personStore.getInitialState(), personStore.getInitialState()) 229 | 230 | personStore.subscribe(render) 231 | ``` 232 | 233 | ## Troubleshooting 234 | 235 | TBD 236 | -------------------------------------------------------------------------------- /docs/middlewares/redux.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: redux 3 | description: How to use actions and reducers in a store 4 | nav: 208 5 | --- 6 | 7 | # redux 8 | 9 | `redux` middleware lets you update a store through actions and reducers just like redux. 10 | 11 | ```js 12 | const nextStateCreatorFn = redux(reducerFn, initialState) 13 | ``` 14 | 15 | - [Types](#types) 16 | - [Signature](#signature) 17 | - [Mutator](#mutator) 18 | - [Reference](#reference) 19 | - [Usage](#usage) 20 | - [Updating state through actions and reducers](#updating-state-through-actions-and-reducers) 21 | - [Troubleshooting](#troubleshooting) 22 | 23 | ## Types 24 | 25 | ### Signature 26 | 27 | ```ts 28 | redux(reducerFn: (state: T, action: A) => T, initialState: T): StateCreator A }, [['zustand/redux', A]], []> 29 | ``` 30 | 31 | ### Mutator 32 | 33 | 34 | ```ts 35 | ['zustand/redux', A] 36 | ``` 37 | 38 | 39 | ## Reference 40 | 41 | ### `redux(reducerFn, initialState)` 42 | 43 | #### Parameters 44 | 45 | - `reducerFn`: It should be pure and should take the current state of your application and an action 46 | object as arguments, and returns the new state resulting from applying the action. 47 | - `initialState`: The value you want the state to be initially. It can be a value of any type, 48 | except a function. 49 | 50 | #### Returns 51 | 52 | `redux` returns a state creator function. 53 | 54 | ## Usage 55 | 56 | ### Updating state through actions and reducers 57 | 58 | ```ts 59 | import { createStore } from 'zustand/vanilla' 60 | import { redux } from 'zustand/middleware' 61 | 62 | type PersonStoreState = { 63 | firstName: string 64 | lastName: string 65 | email: string 66 | } 67 | 68 | type PersonStoreAction = 69 | | { type: 'person/setFirstName'; firstName: string } 70 | | { type: 'person/setLastName'; lastName: string } 71 | | { type: 'person/setEmail'; email: string } 72 | 73 | type PersonStore = PersonStoreState & PersonStoreActions 74 | 75 | const personStoreReducer = ( 76 | state: PersonStoreState, 77 | action: PersonStoreAction, 78 | ) => { 79 | switch (action.type) { 80 | case 'person/setFirstName': { 81 | return { ...state, firstName: action.firstName } 82 | } 83 | case 'person/setLastName': { 84 | return { ...state, lastName: action.lastName } 85 | } 86 | case 'person/setEmail': { 87 | return { ...state, email: action.email } 88 | } 89 | default: { 90 | return state 91 | } 92 | } 93 | } 94 | 95 | const personStoreInitialState: PersonStoreState = { 96 | firstName: 'Barbara', 97 | lastName: 'Hepworth', 98 | email: 'bhepworth@sculpture.com', 99 | } 100 | 101 | const personStore = createStore()( 102 | redux(personStoreReducer, personStoreInitialState), 103 | ) 104 | 105 | const $firstNameInput = document.getElementById( 106 | 'first-name', 107 | ) as HTMLInputElement 108 | const $lastNameInput = document.getElementById('last-name') as HTMLInputElement 109 | const $emailInput = document.getElementById('email') as HTMLInputElement 110 | const $result = document.getElementById('result') as HTMLDivElement 111 | 112 | function handleFirstNameChange(event: Event) { 113 | personStore.dispatch({ 114 | type: 'person/setFirstName', 115 | firstName: (event.target as any).value, 116 | }) 117 | } 118 | 119 | function handleLastNameChange(event: Event) { 120 | personStore.dispatch({ 121 | type: 'person/setLastName', 122 | lastName: (event.target as any).value, 123 | }) 124 | } 125 | 126 | function handleEmailChange(event: Event) { 127 | personStore.dispatch({ 128 | type: 'person/setEmail', 129 | email: (event.target as any).value, 130 | }) 131 | } 132 | 133 | $firstNameInput.addEventListener('input', handleFirstNameChange) 134 | $lastNameInput.addEventListener('input', handleLastNameChange) 135 | $emailInput.addEventListener('input', handleEmailChange) 136 | 137 | const render: Parameters[0] = (person) => { 138 | $firstNameInput.value = person.firstName 139 | $lastNameInput.value = person.lastName 140 | $emailInput.value = person.email 141 | 142 | $result.innerHTML = `${person.firstName} ${person.lastName} (${person.email})` 143 | } 144 | 145 | render(personStore.getInitialState(), personStore.getInitialState()) 146 | 147 | personStore.subscribe(render) 148 | ``` 149 | 150 | Here's the `html` code 151 | 152 | ```html 153 | 157 | 161 | 165 |

166 | ``` 167 | 168 | ## Troubleshooting 169 | 170 | TBD 171 | -------------------------------------------------------------------------------- /docs/middlewares/subscribe-with-selector.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: subscribeWithSelector 3 | description: How to subscribe to granular store updates in a store 4 | nav: 210 5 | --- 6 | 7 | # subscribeWithSelector 8 | 9 | `subscribeWithSelector` middleware lets you subscribe to specific data based on current state. 10 | 11 | ```js 12 | const nextStateCreatorFn = subscribeWithSelector(stateCreatorFn) 13 | ``` 14 | 15 | - [Types](#types) 16 | - [Signature](#signature) 17 | - [Mutator](#mutator) 18 | - [Reference](#reference) 19 | - [Usage](#usage) 20 | - [Troubleshooting](#troubleshooting) 21 | 22 | ## Types 23 | 24 | ### Signature 25 | 26 | ```ts 27 | subscribeWithSelector(stateCreatorFn: StateCreator): StateCreator 28 | ``` 29 | 30 | ### Mutator 31 | 32 | 33 | ```ts 34 | ['zustand/subscribeWithSelector', never] 35 | ``` 36 | 37 | 38 | ## Reference 39 | 40 | ### `subscribeWithSelector(stateCreatorFn)` 41 | 42 | #### Parameters 43 | 44 | - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. 45 | Usually, you will return an object with the methods you want to expose. 46 | 47 | #### Returns 48 | 49 | `subscribeWithSelector` returns a state creator function. 50 | 51 | ## Usage 52 | 53 | ### Subscribing partial state updates 54 | 55 | By subscribing to partial state updates, you register a callback that fires whenever the store's 56 | partial state updates. We can use `subscribe` for external state management. 57 | 58 | ```ts 59 | import { createStore } from 'zustand/vanilla' 60 | import { subscribeWithSelector } from 'zustand/middleware' 61 | 62 | type PositionStoreState = { position: { x: number; y: number } } 63 | 64 | type PositionStoreActions = { 65 | setPosition: (nextPosition: PositionStoreState['position']) => void 66 | } 67 | 68 | type PositionStore = PositionStoreState & PositionStoreActions 69 | 70 | const positionStore = createStore()( 71 | subscribeWithSelector((set) => ({ 72 | position: { x: 0, y: 0 }, 73 | setPosition: (position) => set({ position }), 74 | })), 75 | ) 76 | 77 | const $dot = document.getElementById('dot') as HTMLDivElement 78 | 79 | $dot.addEventListener('mouseenter', (event) => { 80 | const parent = event.currentTarget.parentElement 81 | const parentWidth = parent.clientWidth 82 | const parentHeight = parent.clientHeight 83 | 84 | positionStore.getState().setPosition({ 85 | x: Math.ceil(Math.random() * parentWidth), 86 | y: Math.ceil(Math.random() * parentHeight), 87 | }) 88 | }) 89 | 90 | const render: Parameters[0] = (state) => { 91 | $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` 92 | } 93 | 94 | render(positionStore.getInitialState(), positionStore.getInitialState()) 95 | 96 | positionStore.subscribe((state) => state.position, render) 97 | 98 | const logger: Parameters[0] = (x) => { 99 | console.log('new x position', { x }) 100 | } 101 | 102 | positionStore.subscribe((state) => state.position.x, logger) 103 | ``` 104 | 105 | Here's the `html` code 106 | 107 | ```html 108 |
112 |
116 |
117 | ``` 118 | 119 | ## Troubleshooting 120 | 121 | TBD 122 | -------------------------------------------------------------------------------- /docs/migrations/migrating-to-v5.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to Migrate to v5 from v4 3 | nav: 23 4 | --- 5 | 6 | # How to Migrate to v5 from v4 7 | 8 | We highly recommend to update to the latest version of v4, before migrating to v5. It will show all deprecation warnings without breaking your app. 9 | 10 | ## Changes in v5 11 | 12 | - Drop default exports 13 | - Drop deprecated features 14 | - Make React 18 the minimum required version 15 | - Make use-sync-external-store a peer dependency (required for `createWithEqualityFn` and `useStoreWithEqualityFn` in `zustand/traditional`) 16 | - Make TypeScript 4.5 the minimum required version 17 | - Drop UMD/SystemJS support 18 | - Organize entry points in the package.json 19 | - Drop ES5 support 20 | - Stricter types when setState's replace flag is set 21 | - Persist middleware behavioral change 22 | - Other small improvements (technically breaking changes) 23 | 24 | ## Migration Guide 25 | 26 | ### Using custom equality functions such as `shallow` 27 | 28 | The `create` function in v5 does not support customizing equality function. 29 | 30 | If you use custom equality function such as `shallow`, 31 | the easiest migration is to use `createWithEqualityFn`. 32 | 33 | ```js 34 | // v4 35 | import { create } from 'zustand' 36 | import { shallow } from 'zustand/shallow' 37 | 38 | const useCountStore = create((set) => ({ 39 | count: 0, 40 | text: 'hello', 41 | // ... 42 | })) 43 | 44 | const Component = () => { 45 | const { count, text } = useCountStore( 46 | (state) => ({ 47 | count: state.count, 48 | text: state.text, 49 | }), 50 | shallow, 51 | ) 52 | // ... 53 | } 54 | ``` 55 | 56 | That can be done with `createWithEqualityFn` in v5: 57 | 58 | ```bash 59 | npm install use-sync-external-store 60 | ``` 61 | 62 | ```js 63 | // v5 64 | import { createWithEqualityFn as create } from 'zustand/traditional' 65 | 66 | // The rest is the same as v4 67 | ``` 68 | 69 | Alternatively, for the `shallow` use case, you can use `useShallow` hook: 70 | 71 | ```js 72 | // v5 73 | import { create } from 'zustand' 74 | import { useShallow } from 'zustand/shallow' 75 | 76 | const useCountStore = create((set) => ({ 77 | count: 0, 78 | text: 'hello', 79 | // ... 80 | })) 81 | 82 | const Component = () => { 83 | const { count, text } = useCountStore( 84 | useShallow((state) => ({ 85 | count: state.count, 86 | text: state.text, 87 | })), 88 | ) 89 | // ... 90 | } 91 | ``` 92 | 93 | ### Requiring stable selector outputs 94 | 95 | There is a behavioral change in v5 to match React default behavior. 96 | If a selector returns a new reference, it may cause infinite loops. 97 | 98 | For example, this may cause infinite loops. 99 | 100 | ```js 101 | // v4 102 | const [searchValue, setSearchValue] = useStore((state) => [ 103 | state.searchValue, 104 | state.setSearchValue, 105 | ]) 106 | ``` 107 | 108 | The error message will be something like this: 109 | 110 | ```plaintext 111 | Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. 112 | ``` 113 | 114 | To fix it, use the `useShallow` hook, which will return a stable reference. 115 | 116 | ```js 117 | // v5 118 | import { useShallow } from 'zustand/shallow' 119 | 120 | const [searchValue, setSearchValue] = useStore( 121 | useShallow((state) => [state.searchValue, state.setSearchValue]), 122 | ) 123 | ``` 124 | 125 | Here's another example that may cause infinite loops. 126 | 127 | ```js 128 | // v4 129 | const action = useMainStore((state) => { 130 | return state.action ?? () => {} 131 | }) 132 | ``` 133 | 134 | To fix it, make sure the selector function returns a stable reference. 135 | 136 | ```js 137 | // v5 138 | 139 | const FALLBACK_ACTION = () => {} 140 | 141 | const action = useMainStore((state) => { 142 | return state.action ?? FALLBACK_ACTION 143 | }) 144 | ``` 145 | 146 | Alternatively, if you need v4 behavior, `createWithEqualityFn` will do. 147 | 148 | ```js 149 | // v5 150 | import { createWithEqualityFn as create } from 'zustand/traditional' 151 | ``` 152 | 153 | ### Stricter types when setState's replace flag is set (Typescript only) 154 | 155 | ```diff 156 | - setState: 157 | - (partial: T | Partial | ((state: T) => T | Partial), replace?: boolean | undefined) => void; 158 | + setState: 159 | + (partial: T | Partial | ((state: T) => T | Partial), replace?: false) => void; 160 | + (state: T | ((state: T) => T), replace: true) => void; 161 | ``` 162 | 163 | If you are not using the `replace` flag, no migration is required. 164 | 165 | If you are using the `replace` flag and it's set to `true`, you must provide a complete state object. 166 | This change ensures that `store.setState({}, true)` (which results in an invalid state) is no longer considered valid. 167 | 168 | **Examples:** 169 | 170 | ```ts 171 | // Partial state update (valid) 172 | store.setState({ key: 'value' }) 173 | 174 | // Complete state replacement (valid) 175 | store.setState({ key: 'value' }, true) 176 | 177 | // Incomplete state replacement (invalid) 178 | store.setState({}, true) // Error 179 | ``` 180 | 181 | #### Handling Dynamic `replace` Flag 182 | 183 | If the value of the `replace` flag is dynamic and determined at runtime, you might face issues. To handle this, you can use a workaround by annotating the `replace` parameter with the parameters of the `setState` function: 184 | 185 | ```ts 186 | const replaceFlag = Math.random() > 0.5 187 | const args = [{ bears: 5 }, replaceFlag] as Parameters< 188 | typeof useBearStore.setState 189 | > 190 | store.setState(...args) 191 | ``` 192 | 193 | #### Persist middleware no longer stores item at store creation 194 | 195 | Previously, the `persist` middleware stored the initial state during store creation. This behavior has been removed in v5 (and v4.5.5). 196 | 197 | For example, in the following code, the initial state is stored in the storage. 198 | 199 | ```js 200 | // v4 201 | import { create } from 'zustand' 202 | import { persist } from 'zustand/middleware' 203 | 204 | const useCountStore = create( 205 | persist( 206 | () => ({ 207 | count: Math.floor(Math.random() * 1000), 208 | }), 209 | { 210 | name: 'count', 211 | }, 212 | ), 213 | ) 214 | ``` 215 | 216 | In v5, this is no longer the case, and you need to explicitly set the state after store creation. 217 | 218 | ```js 219 | // v5 220 | import { create } from 'zustand' 221 | import { persist } from 'zustand/middleware' 222 | 223 | const useCountStore = create( 224 | persist( 225 | () => ({ 226 | count: 0, 227 | }), 228 | { 229 | name: 'count', 230 | }, 231 | ), 232 | ) 233 | useCountStore.setState({ 234 | count: Math.floor(Math.random() * 1000), 235 | }) 236 | ``` 237 | 238 | ## Links 239 | 240 | - https://github.com/pmndrs/zustand/pull/2138 241 | - https://github.com/pmndrs/zustand/pull/2580 242 | -------------------------------------------------------------------------------- /docs/previous-versions/zustand-v3-create-context.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: createContext from zustand/context 3 | nav: 21 4 | --- 5 | 6 | A special `createContext` is provided since v3.5, 7 | which avoids misusing the store hook. 8 | 9 | > **Note**: This function is deprecated in v4 and will be removed in v5. See [Migration](#migration). 10 | 11 | ```jsx 12 | import create from 'zustand' 13 | import createContext from 'zustand/context' 14 | 15 | const { Provider, useStore } = createContext() 16 | 17 | const createStore = () => create(...) 18 | 19 | const App = () => ( 20 | 21 | ... 22 | 23 | ) 24 | 25 | const Component = () => { 26 | const state = useStore() 27 | const slice = useStore(selector) 28 | ... 29 | ``` 30 | 31 | ## createContext usage in real components 32 | 33 | ```jsx 34 | import create from "zustand"; 35 | import createContext from "zustand/context"; 36 | 37 | // Best practice: You can move the below createContext() and createStore to a separate file(store.js) and import the Provider, useStore here/wherever you need. 38 | 39 | const { Provider, useStore } = createContext(); 40 | 41 | const createStore = () => 42 | create((set) => ({ 43 | bears: 0, 44 | increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), 45 | removeAllBears: () => set({ bears: 0 }) 46 | })); 47 | 48 | const Button = () => { 49 | return ( 50 | {/** store() - This will create a store for each time using the Button component instead of using one store for all components **/} 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | const ButtonChild = () => { 58 | const state = useStore(); 59 | return ( 60 |
61 | {state.bears} 62 | 69 |
70 | ); 71 | }; 72 | 73 | export default function App() { 74 | return ( 75 |
76 |
79 | ); 80 | } 81 | ``` 82 | 83 | ## createContext usage with initialization from props 84 | 85 | ```tsx 86 | import create from 'zustand' 87 | import createContext from 'zustand/context' 88 | 89 | const { Provider, useStore } = createContext() 90 | 91 | export default function App({ initialBears }) { 92 | return ( 93 | 95 | create((set) => ({ 96 | bears: initialBears, 97 | increase: () => set((state) => ({ bears: state.bears + 1 })), 98 | })) 99 | } 100 | > 101 | 17 | 18 | ) 19 | } 20 | 21 | export default function App() { 22 | return ( 23 | <> 24 | 25 |
26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 |
34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /examples/demo/src/components/CodePreview.jsx: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { Highlight } from 'prism-react-renderer' 3 | import CopyButton from './CopyButton' 4 | import SnippetLang from './SnippetLang' 5 | import javascriptCode from '../resources/javascript-code' 6 | import typescriptCode from '../resources/typescript-code' 7 | 8 | const useStore = create((set, get) => ({ 9 | lang: 'javascript', 10 | setLang: (lang) => set(() => ({ lang })), 11 | getCode: () => 12 | get().lang === 'javascript' ? javascriptCode : typescriptCode, 13 | })) 14 | 15 | export default function CodePreview() { 16 | const { lang, setLang, getCode } = useStore() 17 | const code = getCode() 18 | 19 | return ( 20 | 21 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 22 | // define how each line is to be rendered in the code block, 23 | // position is set to relative so the copy button can align to bottom right 24 |
25 |           {tokens.map((line, i) => (
26 |             
27 | {line.map((token, key) => ( 28 | 29 | ))} 30 |
31 | ))} 32 |
33 | 34 | 35 |
36 |
37 | )} 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /examples/demo/src/components/CopyButton.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useRef } from 'react' 2 | import { copyToClipboard } from '../utils/copy-to-clipboard' 3 | 4 | /* 5 | Isolated logic for the entire copy functionality instead 6 | of a separate button component and with the added utility 7 | */ 8 | export default function CopyButton({ code, ...props }) { 9 | const [isCopied, setIsCopied] = useState(false) 10 | const timer = useRef() 11 | 12 | const handleCopy = useCallback(() => { 13 | clearTimeout(timer.current) 14 | copyToClipboard(code).then(() => { 15 | setIsCopied(true) 16 | timer.current = setTimeout(() => setIsCopied(false), 3000) 17 | }) 18 | }, [code]) 19 | 20 | return ( 21 | <> 22 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /examples/demo/src/components/Details.jsx: -------------------------------------------------------------------------------- 1 | export default function Details() { 2 | return ( 3 | <> 4 | 8 | 22 | Zustand 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /examples/demo/src/components/Fireflies.jsx: -------------------------------------------------------------------------------- 1 | import { Vector3, CatmullRomCurve3 } from 'three' 2 | import { useRef, useMemo } from 'react' 3 | import { extend, useFrame } from '@react-three/fiber' 4 | import * as meshline from 'meshline' 5 | 6 | extend(meshline) 7 | 8 | const r = () => Math.max(0.2, Math.random()) 9 | 10 | function Fatline({ curve, color }) { 11 | const material = useRef() 12 | useFrame( 13 | (state, delta) => 14 | (material.current.uniforms.dashOffset.value -= delta / 100), 15 | ) 16 | return ( 17 | 18 | 19 | 27 | 28 | ) 29 | } 30 | 31 | export default function Fireflies({ count, colors, radius = 10 }) { 32 | const lines = useMemo( 33 | () => 34 | new Array(count).fill().map(() => { 35 | const pos = new Vector3( 36 | Math.sin(0) * radius * r(), 37 | Math.cos(0) * radius * r(), 38 | 0, 39 | ) 40 | const points = new Array(30).fill().map((_, index) => { 41 | const angle = (index / 20) * Math.PI * 2 42 | return pos 43 | .add( 44 | new Vector3( 45 | Math.sin(angle) * radius * r(), 46 | Math.cos(angle) * radius * r(), 47 | 0, 48 | ), 49 | ) 50 | .clone() 51 | }) 52 | const curve = new CatmullRomCurve3(points).getPoints(100) 53 | return { 54 | color: colors[parseInt(colors.length * Math.random())], 55 | curve, 56 | } 57 | }), 58 | [count, radius, colors], 59 | ) 60 | return ( 61 | 62 | {lines.map((props, index) => ( 63 | 64 | ))} 65 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /examples/demo/src/components/Scene.jsx: -------------------------------------------------------------------------------- 1 | import { Mesh, PlaneGeometry, Group, Vector3, MathUtils } from 'three' 2 | import { memo, useRef, useState, useLayoutEffect } from 'react' 3 | import { createRoot, events, extend, useFrame } from '@react-three/fiber' 4 | import { Plane, useAspect, useTexture } from '@react-three/drei' 5 | import { 6 | EffectComposer, 7 | DepthOfField, 8 | Vignette, 9 | } from '@react-three/postprocessing' 10 | import { MaskFunction } from 'postprocessing' 11 | import Fireflies from './Fireflies' 12 | import bgUrl from '../resources/bg.jpg' 13 | import starsUrl from '../resources/stars.png' 14 | import groundUrl from '../resources/ground.png' 15 | import bearUrl from '../resources/bear.png' 16 | import leaves1Url from '../resources/leaves1.png' 17 | import leaves2Url from '../resources/leaves2.png' 18 | import '../materials/layerMaterial' 19 | 20 | function Experience() { 21 | const scaleN = useAspect(1600, 1000, 1.05) 22 | const scaleW = useAspect(2200, 1000, 1.05) 23 | const textures = useTexture([ 24 | bgUrl, 25 | starsUrl, 26 | groundUrl, 27 | bearUrl, 28 | leaves1Url, 29 | leaves2Url, 30 | ]) 31 | const group = useRef() 32 | const layersRef = useRef([]) 33 | const [movement] = useState(() => new Vector3()) 34 | const [temp] = useState(() => new Vector3()) 35 | const layers = [ 36 | { texture: textures[0], x: 0, y: 0, z: 0, factor: 0.005, scale: scaleW }, 37 | { texture: textures[1], x: 0, y: 0, z: 10, factor: 0.005, scale: scaleW }, 38 | { texture: textures[2], x: 0, y: 0, z: 20, scale: scaleW }, 39 | { 40 | texture: textures[3], 41 | x: 0, 42 | y: 0, 43 | z: 30, 44 | scaleFactor: 0.83, 45 | scale: scaleN, 46 | }, 47 | { 48 | texture: textures[4], 49 | x: 0, 50 | y: 0, 51 | z: 40, 52 | factor: 0.03, 53 | scaleFactor: 1, 54 | wiggle: 0.6, 55 | scale: scaleW, 56 | }, 57 | { 58 | texture: textures[5], 59 | x: -20, 60 | y: -20, 61 | z: 49, 62 | factor: 0.04, 63 | scaleFactor: 1.3, 64 | wiggle: 1, 65 | scale: scaleW, 66 | }, 67 | ] 68 | 69 | useFrame((state, delta) => { 70 | movement.lerp(temp.set(state.pointer.x, state.pointer.y * 0.2, 0), 0.2) 71 | group.current.position.x = MathUtils.lerp( 72 | group.current.position.x, 73 | state.pointer.x * 20, 74 | 0.05, 75 | ) 76 | group.current.rotation.x = MathUtils.lerp( 77 | group.current.rotation.x, 78 | state.pointer.y / 20, 79 | 0.05, 80 | ) 81 | group.current.rotation.y = MathUtils.lerp( 82 | group.current.rotation.y, 83 | -state.pointer.x / 2, 84 | 0.05, 85 | ) 86 | layersRef.current[4].uniforms.time.value = 87 | layersRef.current[5].uniforms.time.value += delta 88 | }, 1) 89 | 90 | return ( 91 | 92 | 93 | {layers.map( 94 | ( 95 | { 96 | scale, 97 | texture, 98 | ref, 99 | factor = 0, 100 | scaleFactor = 1, 101 | wiggle = 0, 102 | x, 103 | y, 104 | z, 105 | }, 106 | i, 107 | ) => ( 108 | 115 | (layersRef.current[i] = el)} 120 | wiggle={wiggle} 121 | scale={scaleFactor} 122 | /> 123 | 124 | ), 125 | )} 126 | 127 | ) 128 | } 129 | 130 | function Effects() { 131 | const ref = useRef() 132 | useLayoutEffect(() => { 133 | const maskMaterial = ref.current.maskPass.getFullscreenMaterial() 134 | maskMaterial.maskFunction = MaskFunction.MULTIPLY_RGB_SET_ALPHA 135 | }) 136 | return ( 137 | 138 | 145 | 146 | 147 | ) 148 | } 149 | 150 | function FallbackScene() { 151 | return ( 152 |
165 | Zustand Bear 174 |
175 | ) 176 | } 177 | 178 | export default function Scene() { 179 | const [error, setError] = useState(null) 180 | 181 | if (error) { 182 | return 183 | } 184 | 185 | return ( 186 | 187 | 188 | 189 | 190 | ) 191 | } 192 | 193 | function Canvas({ children, onError }) { 194 | extend({ Mesh, PlaneGeometry, Group }) 195 | const canvas = useRef(null) 196 | const root = useRef(null) 197 | useLayoutEffect(() => { 198 | try { 199 | if (!root.current) { 200 | root.current = createRoot(canvas.current).configure({ 201 | events, 202 | orthographic: true, 203 | gl: { antialias: false }, 204 | camera: { zoom: 5, position: [0, 0, 200], far: 300, near: 50 }, 205 | onCreated: (state) => { 206 | state.events.connect(document.getElementById('root')) 207 | state.setEvents({ 208 | compute: (event, state) => { 209 | state.pointer.set( 210 | (event.clientX / state.size.width) * 2 - 1, 211 | -(event.clientY / state.size.height) * 2 + 1, 212 | ) 213 | state.raycaster.setFromCamera(state.pointer, state.camera) 214 | }, 215 | }) 216 | }, 217 | }) 218 | } 219 | const resize = () => 220 | root.current.configure({ 221 | width: window.innerWidth, 222 | height: window.innerHeight, 223 | }) 224 | window.addEventListener('resize', resize) 225 | root.current.render(children) 226 | return () => window.removeEventListener('resize', resize) 227 | } catch (e) { 228 | onError?.(e) 229 | } 230 | }, [children, onError]) 231 | 232 | return ( 233 | 243 | ) 244 | } 245 | -------------------------------------------------------------------------------- /examples/demo/src/components/SnippetLang.jsx: -------------------------------------------------------------------------------- 1 | export default function SnippetLang({ lang, setLang }) { 2 | return ( 3 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/demo/src/main.jsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import './styles.css' 3 | import './pmndrs.css' 4 | import App from './App' 5 | 6 | createRoot(document.getElementById('root')).render() 7 | -------------------------------------------------------------------------------- /examples/demo/src/materials/layerMaterial.js: -------------------------------------------------------------------------------- 1 | import { shaderMaterial } from '@react-three/drei' 2 | import { extend } from '@react-three/fiber' 3 | 4 | // This material takes care of wiggling and punches a hole into 5 | // alpha regions so that the depth-of-field effect can process the layers. 6 | // Credit: Gianmarco Simone https://twitter.com/ggsimm 7 | 8 | const LayerMaterial = shaderMaterial( 9 | { textr: null, movement: [0, 0, 0], scale: 1, factor: 0, wiggle: 0, time: 0 }, 10 | ` uniform float time; 11 | uniform vec2 resolution; 12 | uniform float wiggle; 13 | varying vec2 vUv; 14 | varying vec3 vNormal; 15 | void main() { 16 | vUv = uv; 17 | vec3 transformed = vec3(position); 18 | if (wiggle > 0.) { 19 | float theta = sin(time + position.y) / 2.0 * wiggle; 20 | float c = cos(theta); 21 | float s = sin(theta); 22 | mat3 m = mat3(c, 0, s, 0, 1, 0, -s, 0, c); 23 | transformed = transformed * m; 24 | vNormal = vNormal * m; 25 | } 26 | gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.); 27 | }`, 28 | ` uniform float time; 29 | uniform vec2 resolution; 30 | uniform float factor; 31 | uniform float scale; 32 | uniform vec3 movement; 33 | uniform sampler2D textr; 34 | varying vec2 vUv; 35 | void main() { 36 | vec2 uv = vUv / scale + movement.xy * factor; 37 | vec4 color = texture2D(textr, uv); 38 | if (color.a < 0.1) discard; 39 | gl_FragColor = vec4(color.rgb, .1); 40 | #include 41 | #include 42 | }`, 43 | ) 44 | 45 | extend({ LayerMaterial }) 46 | -------------------------------------------------------------------------------- /examples/demo/src/pmndrs.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Pmndrs theme for JavaScript, CSS and HTML 3 | * Loosely based on https://marketplace.visualstudio.com/items?itemName=pmndrs.pmndrs 4 | * @author Paul Henschel 5 | */ 6 | 7 | code[class*='language-'], 8 | pre[class*='language-'] { 9 | color: #e4f0fb !important; 10 | background: none !important; 11 | text-shadow: 0 1px rgba(0, 0, 0, 0.3) !important; 12 | font-family: Menlo, Monaco, 'Courier New', monospace !important; 13 | font-size: 0.95em !important; 14 | text-align: left !important; 15 | white-space: pre !important; 16 | word-spacing: normal !important; 17 | word-break: normal !important; 18 | word-wrap: normal !important; 19 | line-height: 1.5 !important; 20 | 21 | -moz-tab-size: 4 !important; 22 | -o-tab-size: 4 !important; 23 | tab-size: 4 !important; 24 | 25 | -webkit-hyphens: none !important; 26 | -moz-hyphens: none !important; 27 | -ms-hyphens: none !important; 28 | hyphens: none !important; 29 | } 30 | 31 | /* Code blocks */ 32 | pre[class*='language-'] { 33 | padding: 3.75em !important; 34 | margin: -2.5em 0 !important; 35 | overflow: auto !important; 36 | border-radius: 0.75em !important; 37 | } 38 | 39 | :not(pre) > code[class*='language-'], 40 | pre[class*='language-'] { 41 | background: #252b37 !important; 42 | } 43 | 44 | /* Inline code */ 45 | :not(pre) > code[class*='language-'] { 46 | padding: 0.1em !important; 47 | border-radius: 0.3em !important; 48 | white-space: normal !important; 49 | } 50 | 51 | .token.comment, 52 | .token.prolog, 53 | .token.doctype, 54 | .token.cdata { 55 | color: #a6accd !important; 56 | } 57 | 58 | .token.punctuation { 59 | color: #e4f0fb !important; 60 | } 61 | 62 | .token.namespace { 63 | opacity: 0.7 !important; 64 | } 65 | 66 | .token.property, 67 | .token.tag, 68 | .token.constant, 69 | .token.symbol, 70 | .token.deleted { 71 | color: #e4f0fb !important; 72 | } 73 | 74 | .token.boolean, 75 | .token.number { 76 | color: #5de4c7 !important; 77 | } 78 | 79 | .token.selector, 80 | .token.attr-value, 81 | .token.string, 82 | .token.char, 83 | .token.builtin, 84 | .token.inserted { 85 | color: #5de4c7 !important; 86 | } 87 | 88 | .token.attr-name, 89 | .token.operator, 90 | .token.entity, 91 | .token.url, 92 | .language-css .token.string, 93 | .style .token.string, 94 | .token.variable { 95 | color: #add7ff !important; 96 | } 97 | 98 | .token.atrule, 99 | .token.function, 100 | .token.class-name { 101 | color: #5de4c7 !important; 102 | } 103 | 104 | .token.keyword { 105 | color: #add7ff !important; 106 | } 107 | 108 | .token.regex, 109 | .token.important { 110 | color: #fffac2 !important; 111 | } 112 | 113 | .token.important, 114 | .token.bold { 115 | font-weight: bold !important; 116 | } 117 | .token.italic { 118 | font-style: italic !important; 119 | } 120 | 121 | .token.entity { 122 | cursor: help !important; 123 | } 124 | -------------------------------------------------------------------------------- /examples/demo/src/resources/bear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/zustand/4d3a0176ca20e9c58cc8f39157f80fb126d299fb/examples/demo/src/resources/bear.png -------------------------------------------------------------------------------- /examples/demo/src/resources/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/zustand/4d3a0176ca20e9c58cc8f39157f80fb126d299fb/examples/demo/src/resources/bg.jpg -------------------------------------------------------------------------------- /examples/demo/src/resources/ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/zustand/4d3a0176ca20e9c58cc8f39157f80fb126d299fb/examples/demo/src/resources/ground.png -------------------------------------------------------------------------------- /examples/demo/src/resources/javascript-code.js: -------------------------------------------------------------------------------- 1 | export default `import { create } from 'zustand' 2 | 3 | const useStore = create((set) => ({ 4 | count: 1, 5 | inc: () => set((state) => ({ count: state.count + 1 })), 6 | })) 7 | 8 | function Counter() { 9 | const { count, inc } = useStore() 10 | return ( 11 |
12 | {count} 13 | 14 |
15 | ) 16 | }` 17 | -------------------------------------------------------------------------------- /examples/demo/src/resources/leaves1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/zustand/4d3a0176ca20e9c58cc8f39157f80fb126d299fb/examples/demo/src/resources/leaves1.png -------------------------------------------------------------------------------- /examples/demo/src/resources/leaves2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/zustand/4d3a0176ca20e9c58cc8f39157f80fb126d299fb/examples/demo/src/resources/leaves2.png -------------------------------------------------------------------------------- /examples/demo/src/resources/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/zustand/4d3a0176ca20e9c58cc8f39157f80fb126d299fb/examples/demo/src/resources/stars.png -------------------------------------------------------------------------------- /examples/demo/src/resources/typescript-code.js: -------------------------------------------------------------------------------- 1 | export default `import { create } from 'zustand' 2 | 3 | type Store = { 4 | count: number 5 | inc: () => void 6 | } 7 | 8 | const useStore = create()((set) => ({ 9 | count: 1, 10 | inc: () => set((state) => ({ count: state.count + 1 })), 11 | })) 12 | 13 | function Counter() { 14 | const { count, inc } = useStore() 15 | return ( 16 |
17 | {count} 18 | 19 |
20 | ) 21 | }` 22 | -------------------------------------------------------------------------------- /examples/demo/src/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body, 7 | #root { 8 | width: 100%; 9 | height: 100%; 10 | margin: 0; 11 | padding: 0; 12 | -webkit-touch-callout: none; 13 | overflow: hidden; 14 | background: #010101; 15 | } 16 | 17 | #root { 18 | overflow: hidden; 19 | } 20 | 21 | body { 22 | font-family: 23 | -apple-system, 24 | BlinkMacSystemFont, 25 | avenir next, 26 | avenir, 27 | helvetica neue, 28 | helvetica, 29 | ubuntu, 30 | roboto, 31 | noto, 32 | segoe ui, 33 | arial, 34 | sans-serif; 35 | } 36 | 37 | .main { 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | width: 100%; 42 | height: 100%; 43 | color: white; 44 | } 45 | 46 | .main > .code { 47 | position: absolute; 48 | right: 10vw; 49 | margin-right: -60px; 50 | width: 640px; 51 | max-width: 80%; 52 | height: 100%; 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | } 57 | 58 | .code-container { 59 | position: relative; 60 | margin-bottom: -60px; 61 | } 62 | 63 | .counter { 64 | position: absolute; 65 | top: -100px; 66 | right: -20px; 67 | color: white; 68 | background: #394a52; 69 | padding: 40px; 70 | border-radius: 10px; 71 | box-shadow: 0 16px 40px -5px rgba(0, 0, 0, 0.5); 72 | width: 120px; 73 | height: 120px; 74 | font-size: 3em; 75 | } 76 | 77 | .counter > span { 78 | position: absolute; 79 | left: 50%; 80 | top: 50%; 81 | margin-top: -15px; 82 | transform: translate3d(-50%, -50%, 0); 83 | } 84 | 85 | .counter > button { 86 | margin: 10px; 87 | padding: 5px 10px; 88 | position: absolute; 89 | left: 0; 90 | bottom: 0; 91 | width: 100px; 92 | border-radius: 5px; 93 | border: solid 2px white; 94 | outline: none; 95 | background: transparent; 96 | color: white; 97 | cursor: pointer; 98 | } 99 | 100 | .code-container pre[class*='language-'] { 101 | margin-top: -50px; 102 | display: inline-block; 103 | width: auto !important; 104 | padding: 40px 50px 40px 45px; 105 | font-size: 0.8rem !important; 106 | border-radius: 10px !important; 107 | box-shadow: 0 16px 40px -5px rgba(0, 0, 0, 1); 108 | white-space: pre-wrap !important; 109 | } 110 | 111 | span.header-left { 112 | font-weight: 700; 113 | text-transform: uppercase; 114 | position: absolute; 115 | display: inline-block; 116 | top: 40px; 117 | left: 40px; 118 | font-size: 3em; 119 | color: white; 120 | line-height: 1em; 121 | } 122 | 123 | a { 124 | font-family: 125 | -apple-system, 126 | BlinkMacSystemFont, 127 | avenir next, 128 | avenir, 129 | helvetica neue, 130 | helvetica, 131 | ubuntu, 132 | roboto, 133 | noto, 134 | segoe ui, 135 | arial, 136 | sans-serif; 137 | font-weight: 400; 138 | font-size: 16px; 139 | color: inherit; 140 | position: absolute; 141 | display: inline; 142 | text-decoration: none; 143 | } 144 | 145 | .nav { 146 | align-items: center; 147 | display: flex; 148 | gap: 16px; 149 | justify-content: flex-end; 150 | left: 40px; 151 | position: fixed; 152 | right: 40px; 153 | top: 40px; 154 | } 155 | 156 | .nav a { 157 | position: relative; 158 | flex: 0 0 auto; 159 | } 160 | 161 | a.bottom-left { 162 | bottom: 40px; 163 | left: 40px; 164 | } 165 | 166 | a.bottom-right { 167 | bottom: 40px; 168 | right: 40px; 169 | } 170 | 171 | .snippet-container { 172 | display: flex; 173 | align-items: center; 174 | gap: 4px; 175 | position: absolute; 176 | bottom: 0; 177 | right: 0; 178 | padding: 5px; 179 | } 180 | 181 | .snippet-lang { 182 | background-color: #272822; 183 | color: #fff; 184 | outline: 0; 185 | border: 0; 186 | } 187 | 188 | .copy-button { 189 | box-shadow: none; 190 | text-decoration: none; 191 | font-size: 14px; 192 | font-family: sans-serif; 193 | line-height: 1; 194 | padding: 12px; 195 | width: auto; 196 | border-radius: 5px; 197 | border: 0; 198 | outline: none; 199 | background: transparent; 200 | color: #f8f9fa; 201 | cursor: pointer; 202 | } 203 | 204 | .copy-button:hover { 205 | background-color: #5f5e5d; 206 | } 207 | 208 | @media only screen and (max-width: 700px) { 209 | span.header-left { 210 | font-size: 1em; 211 | } 212 | .main > .code { 213 | margin-right: -0px; 214 | } 215 | .code-container > pre[class*='language-'] { 216 | font-size: 0.6rem !important; 217 | border-radius: 10px 10px 0 0 !important; 218 | } 219 | .counter { 220 | position: absolute; 221 | top: -120px; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /examples/demo/src/utils/copy-to-clipboard.js: -------------------------------------------------------------------------------- 1 | export const copyToClipboard = (str) => { 2 | return navigator.clipboard.writeText(str) 3 | } 4 | -------------------------------------------------------------------------------- /examples/demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/starter/README.md: -------------------------------------------------------------------------------- 1 | # Starter [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?style=flat-square&logo=stackblitz)](https://stackblitz.com/github/pmndrs/zustand/tree/main/examples/starter) 2 | 3 | ## Set up locally 4 | 5 | ```bash 6 | git clone https://github.com/pmndrs/zustand 7 | 8 | # install project dependencies & build the library 9 | cd zustand && pnpm install 10 | 11 | # move to the examples folder & install dependencies 12 | cd examples/starter && pnpm install 13 | 14 | # start the dev server 15 | pnpm dev 16 | ``` 17 | 18 | ## Set up on `StackBlitz` 19 | 20 | Link: https://stackblitz.com/github/pmndrs/zustand/tree/main/examples/starter 21 | -------------------------------------------------------------------------------- /examples/starter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Zustand Examples | Starter 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "zustand": "^5.0.2", 13 | "react": "^18.3.1", 14 | "react-dom": "^18.3.1" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.0", 18 | "@types/react-dom": "^18.2.0", 19 | "@vitejs/plugin-react-swc": "^3.5.0", 20 | "typescript": "^5.0.0", 21 | "vite": "^5.3.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/starter/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | } 6 | 7 | #root { 8 | display: flex; 9 | place-items: center; 10 | justify-content: center; 11 | 12 | color: #fff; 13 | background-color: #131311; 14 | } 15 | -------------------------------------------------------------------------------- /examples/starter/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { create } from 'zustand' 4 | 5 | import mascot from './assets/zustand-mascot.svg' 6 | 7 | import './index.css' 8 | 9 | type Store = { 10 | count: number 11 | inc: () => void 12 | } 13 | 14 | const useStore = create((set) => ({ 15 | count: 0, 16 | inc: () => set((state) => ({ count: state.count + 1 })), 17 | })) 18 | 19 | const Counter = () => { 20 | const count = useStore((s) => s.count) 21 | const inc = useStore((s) => s.inc) 22 | 23 | return ( 24 | <> 25 | {count} 26 | 32 | 33 | ) 34 | } 35 | 36 | function App() { 37 | return ( 38 |
39 | 40 | Zustand mascot 48 | 49 | 50 |

Zustand Starter

51 | 52 | 53 |
54 | ) 55 | } 56 | 57 | createRoot(document.getElementById('root')!).render( 58 | 59 | 60 | , 61 | ) 62 | -------------------------------------------------------------------------------- /examples/starter/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "moduleResolution": "node", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "jsx": "react-jsx", 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "esnext", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true 18 | }, 19 | "include": ["vite.config.ts", "./src/**/*"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/starter/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zustand", 3 | "description": "🐻 Bear necessities for state management in React", 4 | "private": true, 5 | "type": "commonjs", 6 | "version": "5.0.3", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "typesVersions": { 10 | ">=4.5": { 11 | "esm/*": [ 12 | "esm/*" 13 | ], 14 | "*": [ 15 | "*" 16 | ] 17 | }, 18 | "*": { 19 | "esm/*": [ 20 | "ts_version_4.5_and_above_is_required.d.ts" 21 | ], 22 | "*": [ 23 | "ts_version_4.5_and_above_is_required.d.ts" 24 | ] 25 | } 26 | }, 27 | "exports": { 28 | "./package.json": "./package.json", 29 | ".": { 30 | "import": { 31 | "types": "./esm/index.d.mts", 32 | "default": "./esm/index.mjs" 33 | }, 34 | "default": { 35 | "types": "./index.d.ts", 36 | "default": "./index.js" 37 | } 38 | }, 39 | "./*": { 40 | "import": { 41 | "types": "./esm/*.d.mts", 42 | "default": "./esm/*.mjs" 43 | }, 44 | "default": { 45 | "types": "./*.d.ts", 46 | "default": "./*.js" 47 | } 48 | } 49 | }, 50 | "files": [ 51 | "**" 52 | ], 53 | "sideEffects": false, 54 | "scripts": { 55 | "prebuild": "shx rm -rf dist", 56 | "build": "pnpm run prebuild && pnpm run \"/^build:.*/\" && pnpm run postbuild", 57 | "build:base": "rollup -c", 58 | "build:vanilla": "rollup -c --config-vanilla", 59 | "build:react": "rollup -c --config-react", 60 | "build:middleware": "rollup -c --config-middleware", 61 | "build:middleware:immer": "rollup -c --config-middleware_immer", 62 | "build:shallow": "rollup -c --config-shallow", 63 | "build:vanilla:shallow": "rollup -c --config-vanilla_shallow", 64 | "build:react:shallow": "rollup -c --config-react_shallow", 65 | "build:traditional": "rollup -c --config-traditional", 66 | "postbuild": "pnpm run patch-d-ts && pnpm run copy && pnpm run patch-old-ts && pnpm run patch-esm-ts", 67 | "fix": "pnpm run fix:lint && pnpm run fix:format", 68 | "fix:format": "prettier . --write", 69 | "fix:lint": "eslint . --fix", 70 | "test": "pnpm run \"/^test:.*/\"", 71 | "test:format": "prettier . --list-different", 72 | "test:types": "tsc --noEmit", 73 | "test:lint": "eslint .", 74 | "test:spec": "vitest run", 75 | "patch-d-ts": "node --input-type=module -e \"import { entries } from './rollup.config.mjs'; import shelljs from 'shelljs'; const { find, sed } = shelljs; find('dist/**/*.d.ts').forEach(f => { entries.forEach(({ find, replacement }) => { sed('-i', new RegExp(' from \\'' + find.source.slice(0, -1) + '\\';$'), ' from \\'' + replacement + '\\';', f); }); sed('-i', / from '(\\.[^']+)\\.ts';$/, ' from \\'\\$1\\';', f); });\"", 76 | "copy": "shx cp -r dist/src/* dist/esm && shx cp -r dist/src/* dist && shx rm -rf dist/src && shx rm -rf dist/{src,tests} && shx cp package.json README.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.prettier=undefined;\"", 77 | "patch-old-ts": "shx touch dist/ts_version_4.5_and_above_is_required.d.ts", 78 | "patch-esm-ts": "node -e \"require('shelljs').find('dist/esm/**/*.d.ts').forEach(f=>{var f2=f.replace(/\\.ts$/,'.mts');require('fs').renameSync(f,f2);require('shelljs').sed('-i',/ from '(\\.[^']+)';$/,' from \\'\\$1.mjs\\';',f2);require('shelljs').sed('-i',/^declare module '(\\.[^']+)'/,'declare module \\'\\$1.mjs\\'',f2)})\"" 79 | }, 80 | "engines": { 81 | "node": ">=12.20.0" 82 | }, 83 | "prettier": { 84 | "semi": false, 85 | "singleQuote": true 86 | }, 87 | "repository": { 88 | "type": "git", 89 | "url": "git+https://github.com/pmndrs/zustand.git" 90 | }, 91 | "keywords": [ 92 | "react", 93 | "state", 94 | "manager", 95 | "management", 96 | "redux", 97 | "store" 98 | ], 99 | "author": "Paul Henschel", 100 | "contributors": [ 101 | "Jeremy Holcomb (https://github.com/JeremyRH)", 102 | "Daishi Kato (https://github.com/dai-shi)" 103 | ], 104 | "license": "MIT", 105 | "bugs": { 106 | "url": "https://github.com/pmndrs/zustand/issues" 107 | }, 108 | "homepage": "https://github.com/pmndrs/zustand", 109 | "packageManager": "pnpm@9.15.5", 110 | "devDependencies": { 111 | "@eslint/js": "^9.17.0", 112 | "@redux-devtools/extension": "^3.3.0", 113 | "@rollup/plugin-alias": "^5.1.1", 114 | "@rollup/plugin-node-resolve": "^16.0.0", 115 | "@rollup/plugin-replace": "^6.0.2", 116 | "@rollup/plugin-typescript": "^12.1.2", 117 | "@testing-library/jest-dom": "^6.6.3", 118 | "@testing-library/react": "^16.1.0", 119 | "@types/node": "^22.10.5", 120 | "@types/react": "^19.0.3", 121 | "@types/react-dom": "^19.0.2", 122 | "@types/use-sync-external-store": "^0.0.6", 123 | "@vitest/coverage-v8": "^2.1.8", 124 | "@vitest/eslint-plugin": "^1.1.24", 125 | "@vitest/ui": "^2.1.8", 126 | "esbuild": "^0.24.2", 127 | "eslint": "9.17.0", 128 | "eslint-import-resolver-typescript": "^3.7.0", 129 | "eslint-plugin-import": "^2.31.0", 130 | "eslint-plugin-jest-dom": "^5.5.0", 131 | "eslint-plugin-react": "^7.37.3", 132 | "eslint-plugin-react-compiler": "19.0.0-beta-bafa41b-20250307", 133 | "eslint-plugin-react-hooks": "^5.2.0", 134 | "eslint-plugin-testing-library": "^7.1.1", 135 | "immer": "^10.1.1", 136 | "jsdom": "^25.0.1", 137 | "json": "^11.0.0", 138 | "prettier": "^3.4.2", 139 | "react": "19.0.0", 140 | "react-dom": "19.0.0", 141 | "redux": "^5.0.1", 142 | "rollup": "^4.30.1", 143 | "rollup-plugin-esbuild": "^6.1.1", 144 | "shelljs": "^0.8.5", 145 | "shx": "^0.3.4", 146 | "tslib": "^2.8.1", 147 | "typescript": "^5.7.2", 148 | "typescript-eslint": "^8.19.1", 149 | "use-sync-external-store": "^1.4.0", 150 | "vitest": "^2.1.8" 151 | }, 152 | "peerDependencies": { 153 | "@types/react": ">=18.0.0", 154 | "immer": ">=9.0.6", 155 | "react": ">=18.0.0", 156 | "use-sync-external-store": ">=1.2.0" 157 | }, 158 | "peerDependenciesMeta": { 159 | "@types/react": { 160 | "optional": true 161 | }, 162 | "immer": { 163 | "optional": true 164 | }, 165 | "react": { 166 | "optional": true 167 | }, 168 | "use-sync-external-store": { 169 | "optional": true 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | /* global process*/ 2 | import path from 'path' 3 | import alias from '@rollup/plugin-alias' 4 | import resolve from '@rollup/plugin-node-resolve' 5 | import replace from '@rollup/plugin-replace' 6 | import typescript from '@rollup/plugin-typescript' 7 | import esbuild from 'rollup-plugin-esbuild' 8 | 9 | const extensions = ['.js', '.ts', '.tsx'] 10 | const { root } = path.parse(process.cwd()) 11 | export const entries = [ 12 | { find: /.*\/vanilla\/shallow\.ts$/, replacement: 'zustand/vanilla/shallow' }, 13 | { find: /.*\/react\/shallow\.ts$/, replacement: 'zustand/react/shallow' }, 14 | { find: /.*\/vanilla\.ts$/, replacement: 'zustand/vanilla' }, 15 | { find: /.*\/react\.ts$/, replacement: 'zustand/react' }, 16 | ] 17 | 18 | function external(id) { 19 | return !id.startsWith('.') && !id.startsWith(root) 20 | } 21 | 22 | function getEsbuild() { 23 | return esbuild({ 24 | target: 'es2018', 25 | supported: { 'import-meta': true }, 26 | tsconfig: path.resolve('./tsconfig.json'), 27 | }) 28 | } 29 | 30 | function createDeclarationConfig(input, output) { 31 | return { 32 | input, 33 | output: { 34 | dir: output, 35 | }, 36 | external, 37 | plugins: [ 38 | typescript({ 39 | declaration: true, 40 | emitDeclarationOnly: true, 41 | outDir: output, 42 | }), 43 | ], 44 | } 45 | } 46 | 47 | function createESMConfig(input, output) { 48 | return { 49 | input, 50 | output: { file: output, format: 'esm' }, 51 | external, 52 | plugins: [ 53 | alias({ entries: entries.filter((entry) => !entry.find.test(input)) }), 54 | resolve({ extensions }), 55 | replace({ 56 | ...(output.endsWith('.js') 57 | ? { 58 | 'import.meta.env?.MODE': 'process.env.NODE_ENV', 59 | } 60 | : { 61 | 'import.meta.env?.MODE': 62 | '(import.meta.env ? import.meta.env.MODE : undefined)', 63 | }), 64 | // a workaround for #829 65 | 'use-sync-external-store/shim/with-selector': 66 | 'use-sync-external-store/shim/with-selector.js', 67 | delimiters: ['\\b', '\\b(?!(\\.|/))'], 68 | preventAssignment: true, 69 | }), 70 | getEsbuild(), 71 | ], 72 | } 73 | } 74 | 75 | function createCommonJSConfig(input, output) { 76 | return { 77 | input, 78 | output: { file: output, format: 'cjs' }, 79 | external, 80 | plugins: [ 81 | alias({ entries: entries.filter((entry) => !entry.find.test(input)) }), 82 | resolve({ extensions }), 83 | replace({ 84 | 'import.meta.env?.MODE': 'process.env.NODE_ENV', 85 | delimiters: ['\\b', '\\b(?!(\\.|/))'], 86 | preventAssignment: true, 87 | }), 88 | getEsbuild(), 89 | ], 90 | } 91 | } 92 | 93 | export default function (args) { 94 | let c = Object.keys(args).find((key) => key.startsWith('config-')) 95 | if (c) { 96 | c = c.slice('config-'.length).replace(/_/g, '/') 97 | } else { 98 | c = 'index' 99 | } 100 | return [ 101 | ...(c === 'index' ? [createDeclarationConfig(`src/${c}.ts`, 'dist')] : []), 102 | createCommonJSConfig(`src/${c}.ts`, `dist/${c}.js`), 103 | createESMConfig(`src/${c}.ts`, `dist/esm/${c}.mjs`), 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './vanilla.ts' 2 | export * from './react.ts' 3 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | export * from './middleware/redux.ts' 2 | export * from './middleware/devtools.ts' 3 | export * from './middleware/subscribeWithSelector.ts' 4 | export * from './middleware/combine.ts' 5 | export * from './middleware/persist.ts' 6 | -------------------------------------------------------------------------------- /src/middleware/combine.ts: -------------------------------------------------------------------------------- 1 | import type { StateCreator, StoreMutatorIdentifier } from '../vanilla.ts' 2 | 3 | type Write = Omit & U 4 | 5 | export function combine< 6 | T extends object, 7 | U extends object, 8 | Mps extends [StoreMutatorIdentifier, unknown][] = [], 9 | Mcs extends [StoreMutatorIdentifier, unknown][] = [], 10 | >( 11 | initialState: T, 12 | create: StateCreator, 13 | ): StateCreator, Mps, Mcs> { 14 | return (...args) => Object.assign({}, initialState, (create as any)(...args)) 15 | } 16 | -------------------------------------------------------------------------------- /src/middleware/immer.ts: -------------------------------------------------------------------------------- 1 | import { produce } from 'immer' 2 | import type { Draft } from 'immer' 3 | import type { StateCreator, StoreMutatorIdentifier } from '../vanilla.ts' 4 | 5 | type Immer = < 6 | T, 7 | Mps extends [StoreMutatorIdentifier, unknown][] = [], 8 | Mcs extends [StoreMutatorIdentifier, unknown][] = [], 9 | >( 10 | initializer: StateCreator, 11 | ) => StateCreator 12 | 13 | declare module '../vanilla' { 14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 15 | interface StoreMutators { 16 | ['zustand/immer']: WithImmer 17 | } 18 | } 19 | 20 | type Write = Omit & U 21 | type SkipTwo = T extends { length: 0 } 22 | ? [] 23 | : T extends { length: 1 } 24 | ? [] 25 | : T extends { length: 0 | 1 } 26 | ? [] 27 | : T extends [unknown, unknown, ...infer A] 28 | ? A 29 | : T extends [unknown, unknown?, ...infer A] 30 | ? A 31 | : T extends [unknown?, unknown?, ...infer A] 32 | ? A 33 | : never 34 | 35 | type SetStateType = Exclude any> 36 | 37 | type WithImmer = Write> 38 | 39 | type StoreImmer = S extends { 40 | setState: infer SetState 41 | } 42 | ? SetState extends { 43 | (...args: infer A1): infer Sr1 44 | (...args: infer A2): infer Sr2 45 | } 46 | ? { 47 | // Ideally, we would want to infer the `nextStateOrUpdater` `T` type from the 48 | // `A1` type, but this is infeasible since it is an intersection with 49 | // a partial type. 50 | setState( 51 | nextStateOrUpdater: 52 | | SetStateType 53 | | Partial> 54 | | ((state: Draft>) => void), 55 | shouldReplace?: false, 56 | ...args: SkipTwo 57 | ): Sr1 58 | setState( 59 | nextStateOrUpdater: 60 | | SetStateType 61 | | ((state: Draft>) => void), 62 | shouldReplace: true, 63 | ...args: SkipTwo 64 | ): Sr2 65 | } 66 | : never 67 | : never 68 | 69 | type ImmerImpl = ( 70 | storeInitializer: StateCreator, 71 | ) => StateCreator 72 | 73 | const immerImpl: ImmerImpl = (initializer) => (set, get, store) => { 74 | type T = ReturnType 75 | 76 | store.setState = (updater, replace, ...args) => { 77 | const nextState = ( 78 | typeof updater === 'function' ? produce(updater as any) : updater 79 | ) as ((s: T) => T) | T | Partial 80 | 81 | return set(nextState, replace as any, ...args) 82 | } 83 | 84 | return initializer(store.setState, get, store) 85 | } 86 | 87 | export const immer = immerImpl as unknown as Immer 88 | -------------------------------------------------------------------------------- /src/middleware/redux.ts: -------------------------------------------------------------------------------- 1 | import type { StateCreator, StoreMutatorIdentifier } from '../vanilla.ts' 2 | import type { NamedSet } from './devtools.ts' 3 | 4 | type Write = Omit & U 5 | 6 | type Action = { type: string } 7 | 8 | type StoreRedux = { 9 | dispatch: (a: A) => A 10 | dispatchFromDevtools: true 11 | } 12 | 13 | type ReduxState = { 14 | dispatch: StoreRedux['dispatch'] 15 | } 16 | 17 | type WithRedux = Write> 18 | 19 | type Redux = < 20 | T, 21 | A extends Action, 22 | Cms extends [StoreMutatorIdentifier, unknown][] = [], 23 | >( 24 | reducer: (state: T, action: A) => T, 25 | initialState: T, 26 | ) => StateCreator>, Cms, [['zustand/redux', A]]> 27 | 28 | declare module '../vanilla' { 29 | interface StoreMutators { 30 | 'zustand/redux': WithRedux 31 | } 32 | } 33 | 34 | type ReduxImpl = ( 35 | reducer: (state: T, action: A) => T, 36 | initialState: T, 37 | ) => StateCreator, [], []> 38 | 39 | const reduxImpl: ReduxImpl = (reducer, initial) => (set, _get, api) => { 40 | type S = typeof initial 41 | type A = Parameters[1] 42 | ;(api as any).dispatch = (action: A) => { 43 | ;(set as NamedSet)((state: S) => reducer(state, action), false, action) 44 | return action 45 | } 46 | ;(api as any).dispatchFromDevtools = true 47 | 48 | return { dispatch: (...args) => (api as any).dispatch(...args), ...initial } 49 | } 50 | export const redux = reduxImpl as unknown as Redux 51 | -------------------------------------------------------------------------------- /src/middleware/subscribeWithSelector.ts: -------------------------------------------------------------------------------- 1 | import type { StateCreator, StoreMutatorIdentifier } from '../vanilla.ts' 2 | 3 | type SubscribeWithSelector = < 4 | T, 5 | Mps extends [StoreMutatorIdentifier, unknown][] = [], 6 | Mcs extends [StoreMutatorIdentifier, unknown][] = [], 7 | >( 8 | initializer: StateCreator< 9 | T, 10 | [...Mps, ['zustand/subscribeWithSelector', never]], 11 | Mcs 12 | >, 13 | ) => StateCreator 14 | 15 | type Write = Omit & U 16 | 17 | type WithSelectorSubscribe = S extends { getState: () => infer T } 18 | ? Write> 19 | : never 20 | 21 | declare module '../vanilla' { 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | interface StoreMutators { 24 | ['zustand/subscribeWithSelector']: WithSelectorSubscribe 25 | } 26 | } 27 | 28 | type StoreSubscribeWithSelector = { 29 | subscribe: { 30 | (listener: (selectedState: T, previousSelectedState: T) => void): () => void 31 | ( 32 | selector: (state: T) => U, 33 | listener: (selectedState: U, previousSelectedState: U) => void, 34 | options?: { 35 | equalityFn?: (a: U, b: U) => boolean 36 | fireImmediately?: boolean 37 | }, 38 | ): () => void 39 | } 40 | } 41 | 42 | type SubscribeWithSelectorImpl = ( 43 | storeInitializer: StateCreator, 44 | ) => StateCreator 45 | 46 | const subscribeWithSelectorImpl: SubscribeWithSelectorImpl = 47 | (fn) => (set, get, api) => { 48 | type S = ReturnType 49 | type Listener = (state: S, previousState: S) => void 50 | const origSubscribe = api.subscribe as (listener: Listener) => () => void 51 | api.subscribe = ((selector: any, optListener: any, options: any) => { 52 | let listener: Listener = selector // if no selector 53 | if (optListener) { 54 | const equalityFn = options?.equalityFn || Object.is 55 | let currentSlice = selector(api.getState()) 56 | listener = (state) => { 57 | const nextSlice = selector(state) 58 | if (!equalityFn(currentSlice, nextSlice)) { 59 | const previousSlice = currentSlice 60 | optListener((currentSlice = nextSlice), previousSlice) 61 | } 62 | } 63 | if (options?.fireImmediately) { 64 | optListener(currentSlice, currentSlice) 65 | } 66 | } 67 | return origSubscribe(listener) 68 | }) as any 69 | const initialState = fn(set, get, api) 70 | return initialState 71 | } 72 | export const subscribeWithSelector = 73 | subscribeWithSelectorImpl as unknown as SubscribeWithSelector 74 | -------------------------------------------------------------------------------- /src/react.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createStore } from './vanilla.ts' 3 | import type { 4 | ExtractState, 5 | Mutate, 6 | StateCreator, 7 | StoreApi, 8 | StoreMutatorIdentifier, 9 | } from './vanilla.ts' 10 | 11 | type ReadonlyStoreApi = Pick< 12 | StoreApi, 13 | 'getState' | 'getInitialState' | 'subscribe' 14 | > 15 | 16 | const identity = (arg: T): T => arg 17 | export function useStore>( 18 | api: S, 19 | ): ExtractState 20 | 21 | export function useStore, U>( 22 | api: S, 23 | selector: (state: ExtractState) => U, 24 | ): U 25 | 26 | export function useStore( 27 | api: ReadonlyStoreApi, 28 | selector: (state: TState) => StateSlice = identity as any, 29 | ) { 30 | const slice = React.useSyncExternalStore( 31 | api.subscribe, 32 | () => selector(api.getState()), 33 | () => selector(api.getInitialState()), 34 | ) 35 | React.useDebugValue(slice) 36 | return slice 37 | } 38 | 39 | export type UseBoundStore> = { 40 | (): ExtractState 41 | (selector: (state: ExtractState) => U): U 42 | } & S 43 | 44 | type Create = { 45 | ( 46 | initializer: StateCreator, 47 | ): UseBoundStore, Mos>> 48 | (): ( 49 | initializer: StateCreator, 50 | ) => UseBoundStore, Mos>> 51 | } 52 | 53 | const createImpl = (createState: StateCreator) => { 54 | const api = createStore(createState) 55 | 56 | const useBoundStore: any = (selector?: any) => useStore(api, selector) 57 | 58 | Object.assign(useBoundStore, api) 59 | 60 | return useBoundStore 61 | } 62 | 63 | export const create = ((createState: StateCreator | undefined) => 64 | createState ? createImpl(createState) : createImpl) as Create 65 | -------------------------------------------------------------------------------- /src/react/shallow.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from '../vanilla/shallow.ts' 3 | 4 | export function useShallow(selector: (state: S) => U): (state: S) => U { 5 | const prev = React.useRef(undefined) 6 | return (state) => { 7 | const next = selector(state) 8 | return shallow(prev.current, next) 9 | ? (prev.current as U) 10 | : (prev.current = next) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/shallow.ts: -------------------------------------------------------------------------------- 1 | export { shallow } from './vanilla/shallow.ts' 2 | export { useShallow } from './react/shallow.ts' 3 | -------------------------------------------------------------------------------- /src/traditional.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // eslint-disable-next-line import/extensions 3 | import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector' 4 | import { createStore } from './vanilla.ts' 5 | import type { 6 | Mutate, 7 | StateCreator, 8 | StoreApi, 9 | StoreMutatorIdentifier, 10 | } from './vanilla.ts' 11 | 12 | const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports 13 | 14 | type ExtractState = S extends { getState: () => infer T } ? T : never 15 | 16 | type ReadonlyStoreApi = Pick< 17 | StoreApi, 18 | 'getState' | 'getInitialState' | 'subscribe' 19 | > 20 | 21 | const identity = (arg: T): T => arg 22 | 23 | export function useStoreWithEqualityFn>( 24 | api: S, 25 | ): ExtractState 26 | 27 | export function useStoreWithEqualityFn, U>( 28 | api: S, 29 | selector: (state: ExtractState) => U, 30 | equalityFn?: (a: U, b: U) => boolean, 31 | ): U 32 | 33 | export function useStoreWithEqualityFn( 34 | api: ReadonlyStoreApi, 35 | selector: (state: TState) => StateSlice = identity as any, 36 | equalityFn?: (a: StateSlice, b: StateSlice) => boolean, 37 | ) { 38 | const slice = useSyncExternalStoreWithSelector( 39 | api.subscribe, 40 | api.getState, 41 | api.getInitialState, 42 | selector, 43 | equalityFn, 44 | ) 45 | React.useDebugValue(slice) 46 | return slice 47 | } 48 | 49 | export type UseBoundStoreWithEqualityFn> = { 50 | (): ExtractState 51 | ( 52 | selector: (state: ExtractState) => U, 53 | equalityFn?: (a: U, b: U) => boolean, 54 | ): U 55 | } & S 56 | 57 | type CreateWithEqualityFn = { 58 | ( 59 | initializer: StateCreator, 60 | defaultEqualityFn?: (a: U, b: U) => boolean, 61 | ): UseBoundStoreWithEqualityFn, Mos>> 62 | (): ( 63 | initializer: StateCreator, 64 | defaultEqualityFn?: (a: U, b: U) => boolean, 65 | ) => UseBoundStoreWithEqualityFn, Mos>> 66 | } 67 | 68 | const createWithEqualityFnImpl = ( 69 | createState: StateCreator, 70 | defaultEqualityFn?: (a: U, b: U) => boolean, 71 | ) => { 72 | const api = createStore(createState) 73 | 74 | const useBoundStoreWithEqualityFn: any = ( 75 | selector?: any, 76 | equalityFn = defaultEqualityFn, 77 | ) => useStoreWithEqualityFn(api, selector, equalityFn) 78 | 79 | Object.assign(useBoundStoreWithEqualityFn, api) 80 | 81 | return useBoundStoreWithEqualityFn 82 | } 83 | 84 | export const createWithEqualityFn = (( 85 | createState: StateCreator | undefined, 86 | defaultEqualityFn?: (a: U, b: U) => boolean, 87 | ) => 88 | createState 89 | ? createWithEqualityFnImpl(createState, defaultEqualityFn) 90 | : createWithEqualityFnImpl) as CreateWithEqualityFn 91 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare interface ImportMeta { 2 | env?: { 3 | MODE: string 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/vanilla.ts: -------------------------------------------------------------------------------- 1 | type SetStateInternal = { 2 | _( 3 | partial: T | Partial | { _(state: T): T | Partial }['_'], 4 | replace?: false, 5 | ): void 6 | _(state: T | { _(state: T): T }['_'], replace: true): void 7 | }['_'] 8 | 9 | export interface StoreApi { 10 | setState: SetStateInternal 11 | getState: () => T 12 | getInitialState: () => T 13 | subscribe: (listener: (state: T, prevState: T) => void) => () => void 14 | } 15 | 16 | export type ExtractState = S extends { getState: () => infer T } ? T : never 17 | 18 | type Get = K extends keyof T ? T[K] : F 19 | 20 | export type Mutate = number extends Ms['length' & keyof Ms] 21 | ? S 22 | : Ms extends [] 23 | ? S 24 | : Ms extends [[infer Mi, infer Ma], ...infer Mrs] 25 | ? Mutate[Mi & StoreMutatorIdentifier], Mrs> 26 | : never 27 | 28 | export type StateCreator< 29 | T, 30 | Mis extends [StoreMutatorIdentifier, unknown][] = [], 31 | Mos extends [StoreMutatorIdentifier, unknown][] = [], 32 | U = T, 33 | > = (( 34 | setState: Get, Mis>, 'setState', never>, 35 | getState: Get, Mis>, 'getState', never>, 36 | store: Mutate, Mis>, 37 | ) => U) & { $$storeMutators?: Mos } 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-object-type 40 | export interface StoreMutators {} 41 | export type StoreMutatorIdentifier = keyof StoreMutators 42 | 43 | type CreateStore = { 44 | ( 45 | initializer: StateCreator, 46 | ): Mutate, Mos> 47 | 48 | (): ( 49 | initializer: StateCreator, 50 | ) => Mutate, Mos> 51 | } 52 | 53 | type CreateStoreImpl = < 54 | T, 55 | Mos extends [StoreMutatorIdentifier, unknown][] = [], 56 | >( 57 | initializer: StateCreator, 58 | ) => Mutate, Mos> 59 | 60 | const createStoreImpl: CreateStoreImpl = (createState) => { 61 | type TState = ReturnType 62 | type Listener = (state: TState, prevState: TState) => void 63 | let state: TState 64 | const listeners: Set = new Set() 65 | 66 | const setState: StoreApi['setState'] = (partial, replace) => { 67 | // TODO: Remove type assertion once https://github.com/microsoft/TypeScript/issues/37663 is resolved 68 | // https://github.com/microsoft/TypeScript/issues/37663#issuecomment-759728342 69 | const nextState = 70 | typeof partial === 'function' 71 | ? (partial as (state: TState) => TState)(state) 72 | : partial 73 | if (!Object.is(nextState, state)) { 74 | const previousState = state 75 | state = 76 | (replace ?? (typeof nextState !== 'object' || nextState === null)) 77 | ? (nextState as TState) 78 | : Object.assign({}, state, nextState) 79 | listeners.forEach((listener) => listener(state, previousState)) 80 | } 81 | } 82 | 83 | const getState: StoreApi['getState'] = () => state 84 | 85 | const getInitialState: StoreApi['getInitialState'] = () => 86 | initialState 87 | 88 | const subscribe: StoreApi['subscribe'] = (listener) => { 89 | listeners.add(listener) 90 | // Unsubscribe 91 | return () => listeners.delete(listener) 92 | } 93 | 94 | const api = { setState, getState, getInitialState, subscribe } 95 | const initialState = (state = createState(setState, getState, api)) 96 | return api as any 97 | } 98 | 99 | export const createStore = ((createState) => 100 | createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore 101 | -------------------------------------------------------------------------------- /src/vanilla/shallow.ts: -------------------------------------------------------------------------------- 1 | const isIterable = (obj: object): obj is Iterable => 2 | Symbol.iterator in obj 3 | 4 | const hasIterableEntries = ( 5 | value: Iterable, 6 | ): value is Iterable & { 7 | entries(): Iterable<[unknown, unknown]> 8 | } => 9 | // HACK: avoid checking entries type 10 | 'entries' in value 11 | 12 | const compareEntries = ( 13 | valueA: { entries(): Iterable<[unknown, unknown]> }, 14 | valueB: { entries(): Iterable<[unknown, unknown]> }, 15 | ) => { 16 | const mapA = valueA instanceof Map ? valueA : new Map(valueA.entries()) 17 | const mapB = valueB instanceof Map ? valueB : new Map(valueB.entries()) 18 | if (mapA.size !== mapB.size) { 19 | return false 20 | } 21 | for (const [key, value] of mapA) { 22 | if (!Object.is(value, mapB.get(key))) { 23 | return false 24 | } 25 | } 26 | return true 27 | } 28 | 29 | // Ordered iterables 30 | const compareIterables = ( 31 | valueA: Iterable, 32 | valueB: Iterable, 33 | ) => { 34 | const iteratorA = valueA[Symbol.iterator]() 35 | const iteratorB = valueB[Symbol.iterator]() 36 | let nextA = iteratorA.next() 37 | let nextB = iteratorB.next() 38 | while (!nextA.done && !nextB.done) { 39 | if (!Object.is(nextA.value, nextB.value)) { 40 | return false 41 | } 42 | nextA = iteratorA.next() 43 | nextB = iteratorB.next() 44 | } 45 | return !!nextA.done && !!nextB.done 46 | } 47 | 48 | export function shallow(valueA: T, valueB: T): boolean { 49 | if (Object.is(valueA, valueB)) { 50 | return true 51 | } 52 | if ( 53 | typeof valueA !== 'object' || 54 | valueA === null || 55 | typeof valueB !== 'object' || 56 | valueB === null 57 | ) { 58 | return false 59 | } 60 | if (!isIterable(valueA) || !isIterable(valueB)) { 61 | return compareEntries( 62 | { entries: () => Object.entries(valueA) }, 63 | { entries: () => Object.entries(valueB) }, 64 | ) 65 | } 66 | if (hasIterableEntries(valueA) && hasIterableEntries(valueB)) { 67 | return compareEntries(valueA, valueB) 68 | } 69 | return compareIterables(valueA, valueB) 70 | } 71 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest' 2 | -------------------------------------------------------------------------------- /tests/shallow.test.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { act, fireEvent, render, screen } from '@testing-library/react' 3 | import { beforeEach, describe, expect, it, vi } from 'vitest' 4 | import { create } from 'zustand' 5 | import { useShallow } from 'zustand/react/shallow' 6 | import { createWithEqualityFn } from 'zustand/traditional' 7 | import { shallow } from 'zustand/vanilla/shallow' 8 | 9 | describe('types', () => { 10 | it('works with useBoundStore and array selector (#1107)', () => { 11 | const useBoundStore = createWithEqualityFn(() => ({ 12 | villages: [] as { name: string }[], 13 | })) 14 | const Component = () => { 15 | const villages = useBoundStore((state) => state.villages, shallow) 16 | return <>{villages.length} 17 | } 18 | expect(Component).toBeDefined() 19 | }) 20 | 21 | it('works with useBoundStore and string selector (#1107)', () => { 22 | const useBoundStore = createWithEqualityFn(() => ({ 23 | refetchTimestamp: '', 24 | })) 25 | const Component = () => { 26 | const refetchTimestamp = useBoundStore( 27 | (state) => state.refetchTimestamp, 28 | shallow, 29 | ) 30 | return <>{refetchTimestamp.toUpperCase()} 31 | } 32 | expect(Component).toBeDefined() 33 | }) 34 | }) 35 | 36 | describe('useShallow', () => { 37 | const testUseShallowSimpleCallback = vi.fn() 38 | const TestUseShallowSimple = ({ 39 | selector, 40 | state, 41 | }: { 42 | state: Record 43 | selector: (state: Record) => string[] 44 | }) => { 45 | const selectorOutput = selector(state) 46 | const useShallowOutput = useShallow(selector)(state) 47 | 48 | return ( 49 |
52 | testUseShallowSimpleCallback({ selectorOutput, useShallowOutput }) 53 | } 54 | /> 55 | ) 56 | } 57 | 58 | beforeEach(() => { 59 | testUseShallowSimpleCallback.mockClear() 60 | }) 61 | 62 | it('input and output selectors always return shallow equal values', () => { 63 | const { rerender } = render( 64 | , 65 | ) 66 | 67 | expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(0) 68 | fireEvent.click(screen.getByTestId('test-shallow')) 69 | 70 | const firstRender = testUseShallowSimpleCallback.mock.lastCall?.[0] 71 | 72 | expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(1) 73 | expect(firstRender).toBeTruthy() 74 | expect(firstRender?.selectorOutput).toEqual(firstRender?.useShallowOutput) 75 | 76 | rerender( 77 | , 81 | ) 82 | 83 | fireEvent.click(screen.getByTestId('test-shallow')) 84 | expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(2) 85 | 86 | const secondRender = testUseShallowSimpleCallback.mock.lastCall?.[0] 87 | 88 | expect(secondRender).toBeTruthy() 89 | expect(secondRender?.selectorOutput).toEqual(secondRender?.useShallowOutput) 90 | }) 91 | 92 | it('returns the previously computed instance when possible', () => { 93 | const state = { a: 1, b: 2 } 94 | const { rerender } = render( 95 | , 96 | ) 97 | 98 | fireEvent.click(screen.getByTestId('test-shallow')) 99 | expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(1) 100 | const output1 = 101 | testUseShallowSimpleCallback.mock.lastCall?.[0]?.useShallowOutput 102 | expect(output1).toBeTruthy() 103 | 104 | // Change selector, same output 105 | rerender( 106 | Object.keys(state)} 109 | />, 110 | ) 111 | 112 | fireEvent.click(screen.getByTestId('test-shallow')) 113 | expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(2) 114 | 115 | const output2 = 116 | testUseShallowSimpleCallback.mock.lastCall?.[0]?.useShallowOutput 117 | expect(output2).toBeTruthy() 118 | 119 | expect(output2).toBe(output1) 120 | }) 121 | 122 | it('only re-renders if selector output has changed according to shallow', () => { 123 | let countRenders = 0 124 | const useMyStore = create( 125 | (): Record => ({ a: 1, b: 2, c: 3 }), 126 | ) 127 | const TestShallow = ({ 128 | selector = (state) => Object.keys(state).sort(), 129 | }: { 130 | selector?: (state: Record) => string[] 131 | }) => { 132 | const output = useMyStore(useShallow(selector)) 133 | 134 | ++countRenders 135 | 136 | return
{output.join(',')}
137 | } 138 | 139 | expect(countRenders).toBe(0) 140 | render() 141 | 142 | expect(countRenders).toBe(1) 143 | expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c') 144 | 145 | act(() => { 146 | useMyStore.setState({ a: 4 }) // This will not cause a re-render. 147 | }) 148 | 149 | expect(countRenders).toBe(1) 150 | 151 | act(() => { 152 | useMyStore.setState({ d: 10 }) // This will cause a re-render. 153 | }) 154 | 155 | expect(countRenders).toBe(2) 156 | expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c,d') 157 | }) 158 | 159 | it('does not cause stale closure issues', () => { 160 | const useMyStore = create( 161 | (): Record => ({ a: 1, b: 2, c: 3 }), 162 | ) 163 | const TestShallowWithState = () => { 164 | const [count, setCount] = useState(0) 165 | const output = useMyStore( 166 | useShallow((state) => Object.keys(state).concat([count.toString()])), 167 | ) 168 | 169 | return ( 170 |
setCount((prev) => ++prev)} 173 | > 174 | {output.join(',')} 175 |
176 | ) 177 | } 178 | 179 | render() 180 | 181 | expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c,0') 182 | 183 | fireEvent.click(screen.getByTestId('test-shallow')) 184 | 185 | expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c,1') 186 | }) 187 | }) 188 | -------------------------------------------------------------------------------- /tests/ssr.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { act, screen } from '@testing-library/react' 3 | import { renderToString } from 'react-dom/server' 4 | import { describe, expect, it, vi } from 'vitest' 5 | import { create } from 'zustand' 6 | 7 | interface BearStoreState { 8 | bears: number 9 | } 10 | 11 | interface BearStoreAction { 12 | increasePopulation: () => void 13 | } 14 | 15 | const initialState = { bears: 0 } 16 | const useBearStore = create((set) => ({ 17 | ...initialState, 18 | increasePopulation: () => set(({ bears }) => ({ bears: bears + 1 })), 19 | })) 20 | 21 | function Counter() { 22 | const bears = useBearStore(({ bears }) => bears) 23 | const increasePopulation = useBearStore( 24 | ({ increasePopulation }) => increasePopulation, 25 | ) 26 | 27 | useEffect(() => { 28 | increasePopulation() 29 | }, [increasePopulation]) 30 | 31 | return
bears: {bears}
32 | } 33 | 34 | describe.skipIf(!React.version.startsWith('18'))( 35 | 'ssr behavior with react 18', 36 | () => { 37 | it('should handle different states between server and client correctly', async () => { 38 | const { hydrateRoot } = 39 | await vi.importActual( 40 | 'react-dom/client', 41 | ) 42 | 43 | const view = renderToString( 44 | Loading...
}> 45 | 46 | , 47 | ) 48 | 49 | const container = document.createElement('div') 50 | document.body.appendChild(container) 51 | container.innerHTML = view 52 | 53 | expect(container).toHaveTextContent(/bears: 0/) 54 | 55 | await act(async () => { 56 | hydrateRoot( 57 | container, 58 | Loading...}> 59 | 60 | , 61 | ) 62 | }) 63 | 64 | const bearCountText = await screen.findByText('bears: 1') 65 | expect(bearCountText).toBeInTheDocument() 66 | document.body.removeChild(container) 67 | }) 68 | it('should not have hydration errors', async () => { 69 | const useStore = create(() => ({ 70 | bears: 0, 71 | })) 72 | 73 | const { hydrateRoot } = 74 | await vi.importActual( 75 | 'react-dom/client', 76 | ) 77 | 78 | const Component = () => { 79 | const bears = useStore((state) => state.bears) 80 | return
bears: {bears}
81 | } 82 | 83 | const view = renderToString( 84 | Loading...}> 85 | 86 | , 87 | ) 88 | 89 | const container = document.createElement('div') 90 | document.body.appendChild(container) 91 | container.innerHTML = view 92 | 93 | expect(container).toHaveTextContent(/bears: 0/) 94 | 95 | const consoleMock = vi.spyOn(console, 'error') 96 | 97 | const hydratePromise = act(async () => { 98 | hydrateRoot( 99 | container, 100 | Loading...}> 101 | 102 | , 103 | ) 104 | }) 105 | 106 | // set state during hydration 107 | useStore.setState({ bears: 1 }) 108 | 109 | await hydratePromise 110 | 111 | expect(consoleMock).toHaveBeenCalledTimes(0) 112 | 113 | const bearCountText = await screen.findByText('bears: 1') 114 | expect(bearCountText).toBeInTheDocument() 115 | document.body.removeChild(container) 116 | }) 117 | }, 118 | ) 119 | -------------------------------------------------------------------------------- /tests/subscribe.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { create } from 'zustand' 3 | 4 | describe('subscribe()', () => { 5 | it('should correctly have access to subscribe', () => { 6 | const { subscribe } = create(() => ({ value: 1 })) 7 | expect(typeof subscribe).toBe('function') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /tests/test-utils.ts: -------------------------------------------------------------------------------- 1 | type ReplacedMap = { 2 | type: 'Map' 3 | value: [string, unknown][] 4 | } 5 | 6 | export const replacer = ( 7 | key: string, 8 | value: unknown, 9 | ): ReplacedMap | unknown => { 10 | if (value instanceof Map) { 11 | return { 12 | type: 'Map', 13 | value: Array.from(value.entries()), 14 | } 15 | } else { 16 | return value 17 | } 18 | } 19 | 20 | export const reviver = (key: string, value: ReplacedMap | unknown): unknown => { 21 | if (isReplacedMap(value)) { 22 | return new Map(value.value) 23 | } 24 | return value 25 | } 26 | 27 | const isReplacedMap = (value: any): value is ReplacedMap => { 28 | if (value && value.type === 'Map') { 29 | return true 30 | } 31 | 32 | return false 33 | } 34 | -------------------------------------------------------------------------------- /tests/types.test.tsx: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { create } from 'zustand' 3 | import type { 4 | StateCreator, 5 | StoreApi, 6 | StoreMutatorIdentifier, 7 | UseBoundStore, 8 | } from 'zustand' 9 | import { persist } from 'zustand/middleware' 10 | 11 | it('can use exposed types', () => { 12 | type ExampleState = { 13 | num: number 14 | numGet: () => number 15 | numGetState: () => number 16 | numSet: (v: number) => void 17 | numSetState: (v: number) => void 18 | } 19 | 20 | const listener = (state: ExampleState) => { 21 | if (state) { 22 | const value = state.num * state.numGet() * state.numGetState() 23 | state.numSet(value) 24 | state.numSetState(value) 25 | } 26 | } 27 | const selector = (state: ExampleState) => state.num 28 | const partial: Partial = { 29 | num: 2, 30 | numGet: () => 2, 31 | } 32 | const partialFn: (state: ExampleState) => Partial = ( 33 | state, 34 | ) => ({ 35 | ...state, 36 | num: 2, 37 | }) 38 | const equalityFn = (state: ExampleState, newState: ExampleState) => 39 | state !== newState 40 | 41 | const storeApi = create((set, get) => ({ 42 | num: 1, 43 | numGet: () => get().num, 44 | numGetState: () => { 45 | // TypeScript can't get the type of storeApi when it trys to enforce the signature of numGetState. 46 | // Need to explicitly state the type of storeApi.getState().num or storeApi type will be type 'any'. 47 | const result: number = storeApi.getState().num 48 | return result 49 | }, 50 | numSet: (v) => { 51 | set({ num: v }) 52 | }, 53 | numSetState: (v) => { 54 | storeApi.setState({ num: v }) 55 | }, 56 | })) 57 | const useBoundStore = storeApi 58 | 59 | const stateCreator: StateCreator = (set, get) => ({ 60 | num: 1, 61 | numGet: () => get().num, 62 | numGetState: () => get().num, 63 | numSet: (v) => { 64 | set({ num: v }) 65 | }, 66 | numSetState: (v) => { 67 | set({ num: v }) 68 | }, 69 | }) 70 | 71 | function checkAllTypes( 72 | _getState: StoreApi['getState'], 73 | _partialState: 74 | | Partial 75 | | ((s: ExampleState) => Partial), 76 | _setState: StoreApi['setState'], 77 | _state: object, 78 | _stateListener: (state: ExampleState, previousState: ExampleState) => void, 79 | _stateSelector: (state: ExampleState) => number, 80 | _storeApi: StoreApi, 81 | _subscribe: StoreApi['subscribe'], 82 | _equalityFn: (a: ExampleState, b: ExampleState) => boolean, 83 | _stateCreator: StateCreator, 84 | _useBoundStore: UseBoundStore>, 85 | ) { 86 | expect(true).toBeTruthy() 87 | } 88 | 89 | checkAllTypes( 90 | storeApi.getState, 91 | Math.random() > 0.5 ? partial : partialFn, 92 | storeApi.setState, 93 | storeApi.getState(), 94 | listener, 95 | selector, 96 | storeApi, 97 | storeApi.subscribe, 98 | equalityFn, 99 | stateCreator, 100 | useBoundStore, 101 | ) 102 | }) 103 | 104 | type AssertEqual = Type extends Expected 105 | ? Expected extends Type 106 | ? true 107 | : never 108 | : never 109 | 110 | it('should have correct (partial) types for setState', () => { 111 | type Count = { count: number } 112 | 113 | const store = create((set) => ({ 114 | count: 0, 115 | // @ts-expect-error we shouldn't be able to set count to undefined 116 | a: () => set(() => ({ count: undefined })), 117 | // @ts-expect-error we shouldn't be able to set count to undefined 118 | b: () => set({ count: undefined }), 119 | c: () => set({ count: 1 }), 120 | })) 121 | 122 | const setState: AssertEqual< 123 | typeof store.setState, 124 | StoreApi['setState'] 125 | > = true 126 | expect(setState).toEqual(true) 127 | 128 | // ok, should not error 129 | store.setState({ count: 1 }) 130 | store.setState({}) 131 | store.setState((previous) => previous) 132 | 133 | // @ts-expect-error type undefined is not assignable to type number 134 | store.setState({ count: undefined }) 135 | // @ts-expect-error type undefined is not assignable to type number 136 | store.setState((state) => ({ ...state, count: undefined })) 137 | }) 138 | 139 | it('should allow for different partial keys to be returnable from setState', () => { 140 | type State = { 141 | count: number 142 | something: string 143 | } 144 | 145 | const store = create(() => ({ 146 | count: 0, 147 | something: 'foo', 148 | })) 149 | 150 | const setState: AssertEqual< 151 | typeof store.setState, 152 | StoreApi['setState'] 153 | > = true 154 | expect(setState).toEqual(true) 155 | 156 | // ok, should not error 157 | store.setState((previous) => { 158 | if (previous.count === 0) { 159 | return { count: 1 } 160 | } 161 | return { count: 0 } 162 | }) 163 | store.setState((previous) => { 164 | if (previous.count === 0) { 165 | return { count: 1 } 166 | } 167 | if (previous.count === 1) { 168 | return previous 169 | } 170 | return { something: 'foo' } 171 | }) 172 | 173 | // @ts-expect-error Type '{ something: boolean; count?: undefined; }' is not assignable to type 'State'. 174 | store.setState((previous) => { 175 | if (previous.count === 0) { 176 | return { count: 1 } 177 | } 178 | return { something: true } 179 | }) 180 | }) 181 | 182 | it('state is covariant', () => { 183 | const store = create<{ count: number; foo: string }>()(() => ({ 184 | count: 0, 185 | foo: '', 186 | })) 187 | 188 | const _testIsCovariant: StoreApi<{ count: number }> = store 189 | 190 | // @ts-expect-error should not compile 191 | const _testIsNotContravariant: StoreApi<{ 192 | count: number 193 | foo: string 194 | baz: string 195 | }> = store 196 | }) 197 | 198 | it('StateCreator is StateCreator', () => { 199 | interface State { 200 | count: number 201 | increment: () => void 202 | } 203 | 204 | const foo: () => StateCreator< 205 | State, 206 | M 207 | > = () => (set, get) => ({ 208 | count: 0, 209 | increment: () => { 210 | set({ count: get().count + 1 }) 211 | }, 212 | }) 213 | 214 | create()(persist(foo(), { name: 'prefix' })) 215 | }) 216 | 217 | it('StateCreator subtyping', () => { 218 | interface State { 219 | count: number 220 | increment: () => void 221 | } 222 | 223 | const foo: () => StateCreator = () => (set, get) => ({ 224 | count: 0, 225 | increment: () => { 226 | set({ count: get().count + 1 }) 227 | }, 228 | }) 229 | 230 | create()(persist(foo(), { name: 'prefix' })) 231 | 232 | const _testSubtyping: StateCreator = 233 | {} as StateCreator 234 | }) 235 | 236 | it('set state exists on store with readonly store', () => { 237 | interface State { 238 | count: number 239 | increment: () => void 240 | } 241 | 242 | const useStore = create()((set, get) => ({ 243 | count: 0, 244 | increment: () => set({ count: get().count + 1 }), 245 | })) 246 | 247 | useStore.setState((state) => ({ ...state, count: state.count + 1 })) 248 | }) 249 | -------------------------------------------------------------------------------- /tests/vanilla/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, expect, it, vi } from 'vitest' 2 | import { createStore } from 'zustand/vanilla' 3 | import type { StoreApi } from 'zustand/vanilla' 4 | 5 | // To avoid include react deps on vanilla version 6 | vi.mock('react', () => ({})) 7 | 8 | const consoleError = console.error 9 | afterEach(() => { 10 | console.error = consoleError 11 | }) 12 | 13 | it('create a store', () => { 14 | let params 15 | const result = createStore((...args) => { 16 | params = args 17 | return { value: null } 18 | }) 19 | expect({ params, result }).toMatchInlineSnapshot(` 20 | { 21 | "params": [ 22 | [Function], 23 | [Function], 24 | { 25 | "getInitialState": [Function], 26 | "getState": [Function], 27 | "setState": [Function], 28 | "subscribe": [Function], 29 | }, 30 | ], 31 | "result": { 32 | "getInitialState": [Function], 33 | "getState": [Function], 34 | "setState": [Function], 35 | "subscribe": [Function], 36 | }, 37 | } 38 | `) 39 | }) 40 | 41 | type CounterState = { 42 | count: number 43 | inc: () => void 44 | } 45 | 46 | it('uses the store', async () => { 47 | const store = createStore((set) => ({ 48 | count: 0, 49 | inc: () => set((state) => ({ count: state.count + 1 })), 50 | })) 51 | store.getState().inc() 52 | 53 | expect(store.getState().count).toBe(1) 54 | }) 55 | 56 | it('can get the store', async () => { 57 | type State = { 58 | value: number 59 | getState1: () => State 60 | getState2: () => State 61 | } 62 | 63 | const store = createStore((_, get) => ({ 64 | value: 1, 65 | getState1: () => get(), 66 | getState2: (): State => store.getState(), 67 | })) 68 | 69 | expect(store.getState().getState1().value).toBe(1) 70 | expect(store.getState().getState2().value).toBe(1) 71 | }) 72 | 73 | it('can set the store', async () => { 74 | type State = { 75 | value: number 76 | setState1: StoreApi['setState'] 77 | setState2: StoreApi['setState'] 78 | } 79 | 80 | const store = createStore((set) => ({ 81 | value: 1, 82 | setState1: (v) => set(v), 83 | setState2: (v): void => store.setState(v), 84 | })) 85 | 86 | store.getState().setState1({ value: 2 }) 87 | expect(store.getState().value).toBe(2) 88 | store.getState().setState2({ value: 3 }) 89 | expect(store.getState().value).toBe(3) 90 | }) 91 | 92 | it('both NaN should not update', () => { 93 | const store = createStore(() => NaN) 94 | const fn = vi.fn() 95 | 96 | store.subscribe(fn) 97 | store.setState(NaN) 98 | 99 | expect(fn).not.toBeCalled() 100 | }) 101 | 102 | it('can set the store without merging', () => { 103 | const { setState, getState } = createStore<{ a: number } | { b: number }>( 104 | (_set) => ({ 105 | a: 1, 106 | }), 107 | ) 108 | 109 | // Should override the state instead of merging. 110 | setState({ b: 2 }, true) 111 | 112 | expect(getState()).toEqual({ b: 2 }) 113 | }) 114 | 115 | it('can set the object store to null', () => { 116 | const { setState, getState } = createStore<{ a: number } | null>(() => ({ 117 | a: 1, 118 | })) 119 | 120 | setState(null) 121 | 122 | expect(getState()).toEqual(null) 123 | }) 124 | 125 | it('can set the non-object store to null', () => { 126 | const { setState, getState } = createStore(() => 'value') 127 | 128 | setState(null) 129 | 130 | expect(getState()).toEqual(null) 131 | }) 132 | 133 | it('works with non-object state', () => { 134 | const store = createStore(() => 1) 135 | const inc = () => store.setState((c) => c + 1) 136 | 137 | inc() 138 | 139 | expect(store.getState()).toBe(2) 140 | }) 141 | -------------------------------------------------------------------------------- /tests/vanilla/shallow.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { shallow } from 'zustand/vanilla/shallow' 3 | 4 | describe('shallow', () => { 5 | it('compares primitive values', () => { 6 | expect(shallow(true, true)).toBe(true) 7 | expect(shallow(true, false)).toBe(false) 8 | 9 | expect(shallow(1, 1)).toBe(true) 10 | expect(shallow(1, 2)).toBe(false) 11 | 12 | expect(shallow('zustand', 'zustand')).toBe(true) 13 | expect(shallow('zustand', 'redux')).toBe(false) 14 | }) 15 | 16 | it('compares objects', () => { 17 | expect(shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', asd: 123 })).toBe( 18 | true, 19 | ) 20 | 21 | expect( 22 | shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', foobar: true }), 23 | ).toBe(false) 24 | 25 | expect( 26 | shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', asd: 123, foobar: true }), 27 | ).toBe(false) 28 | }) 29 | 30 | it('compares arrays', () => { 31 | expect(shallow([1, 2, 3], [1, 2, 3])).toBe(true) 32 | 33 | expect(shallow([1, 2, 3], [2, 3, 4])).toBe(false) 34 | 35 | expect( 36 | shallow([{ foo: 'bar' }, { asd: 123 }], [{ foo: 'bar' }, { asd: 123 }]), 37 | ).toBe(false) 38 | 39 | expect(shallow([{ foo: 'bar' }], [{ foo: 'bar', asd: 123 }])).toBe(false) 40 | 41 | expect(shallow([1, 2, 3], [2, 3, 1])).toBe(false) 42 | }) 43 | 44 | it('compares Maps', () => { 45 | expect( 46 | shallow( 47 | new Map([ 48 | ['foo', 'bar'], 49 | ['asd', 123], 50 | ]), 51 | new Map([ 52 | ['foo', 'bar'], 53 | ['asd', 123], 54 | ]), 55 | ), 56 | ).toBe(true) 57 | 58 | expect( 59 | shallow( 60 | new Map([ 61 | ['foo', 'bar'], 62 | ['asd', 123], 63 | ]), 64 | new Map([ 65 | ['asd', 123], 66 | ['foo', 'bar'], 67 | ]), 68 | ), 69 | ).toBe(true) 70 | 71 | expect( 72 | shallow( 73 | new Map([ 74 | ['foo', 'bar'], 75 | ['asd', 123], 76 | ]), 77 | new Map([ 78 | ['foo', 'bar'], 79 | ['foobar', true], 80 | ]), 81 | ), 82 | ).toBe(false) 83 | 84 | expect( 85 | shallow( 86 | new Map([ 87 | ['foo', 'bar'], 88 | ['asd', 123], 89 | ]), 90 | new Map([ 91 | ['foo', 'bar'], 92 | ['asd', 123], 93 | ['foobar', true], 94 | ]), 95 | ), 96 | ).toBe(false) 97 | 98 | const obj = {} 99 | const obj2 = {} 100 | expect( 101 | shallow( 102 | new Map([[obj, 'foo']]), 103 | new Map([[obj2, 'foo']]), 104 | ), 105 | ).toBe(false) 106 | }) 107 | 108 | it('compares Sets', () => { 109 | expect(shallow(new Set(['bar', 123]), new Set(['bar', 123]))).toBe(true) 110 | 111 | expect(shallow(new Set(['bar', 123]), new Set([123, 'bar']))).toBe(true) 112 | 113 | expect(shallow(new Set(['bar', 123]), new Set(['bar', 2]))).toBe(false) 114 | 115 | expect(shallow(new Set(['bar', 123]), new Set(['bar', 123, true]))).toBe( 116 | false, 117 | ) 118 | 119 | const obj = {} 120 | const obj2 = {} 121 | expect(shallow(new Set([obj]), new Set([obj]))).toBe(true) 122 | expect(shallow(new Set([obj]), new Set([obj2]))).toBe(false) 123 | expect(shallow(new Set([obj]), new Set([obj, obj2]))).toBe(false) 124 | expect(shallow(new Set([obj]), new Set([obj2, obj]))).toBe(false) 125 | 126 | expect(shallow(['bar', 123] as never, new Set(['bar', 123]))).toBe(false) 127 | }) 128 | 129 | it('compares functions', () => { 130 | function firstFnCompare() { 131 | return { foo: 'bar' } 132 | } 133 | 134 | function secondFnCompare() { 135 | return { foo: 'bar' } 136 | } 137 | 138 | expect(shallow(firstFnCompare, firstFnCompare)).toBe(true) 139 | 140 | expect(shallow(secondFnCompare, secondFnCompare)).toBe(true) 141 | 142 | expect(shallow(firstFnCompare, secondFnCompare)).toBe(false) 143 | }) 144 | 145 | it('compares URLSearchParams', () => { 146 | expect( 147 | shallow(new URLSearchParams({ a: 'a' }), new URLSearchParams({ a: 'a' })), 148 | ).toBe(true) 149 | expect( 150 | shallow(new URLSearchParams({ a: 'a' }), new URLSearchParams({ a: 'b' })), 151 | ).toBe(false) 152 | expect( 153 | shallow(new URLSearchParams({ a: 'a' }), new URLSearchParams({ b: 'b' })), 154 | ).toBe(false) 155 | expect( 156 | shallow( 157 | new URLSearchParams({ a: 'a' }), 158 | new URLSearchParams({ a: 'a', b: 'b' }), 159 | ), 160 | ).toBe(false) 161 | expect( 162 | shallow( 163 | new URLSearchParams({ b: 'b', a: 'a' }), 164 | new URLSearchParams({ a: 'a', b: 'b' }), 165 | ), 166 | ).toBe(true) 167 | }) 168 | 169 | it('should work with nested arrays (#2794)', () => { 170 | const arr = [1, 2] 171 | expect(shallow([arr, 1], [arr, 1])).toBe(true) 172 | }) 173 | }) 174 | 175 | describe('generators', () => { 176 | it('pure iterable', () => { 177 | function* gen() { 178 | yield 1 179 | yield 2 180 | } 181 | expect(Symbol.iterator in gen()).toBe(true) 182 | expect(shallow(gen(), gen())).toBe(true) 183 | }) 184 | }) 185 | 186 | describe('unsupported cases', () => { 187 | it('date', () => { 188 | expect( 189 | shallow( 190 | new Date('2022-07-19T00:00:00.000Z'), 191 | new Date('2022-07-20T00:00:00.000Z'), 192 | ), 193 | ).not.toBe(false) 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /tests/vanilla/subscribe.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | import { subscribeWithSelector } from 'zustand/middleware' 3 | import { createStore } from 'zustand/vanilla' 4 | 5 | describe('subscribe()', () => { 6 | it('should not be called if new state identity is the same', () => { 7 | const spy = vi.fn() 8 | const initialState = { value: 1, other: 'a' } 9 | const { setState, subscribe } = createStore(() => initialState) 10 | 11 | subscribe(spy) 12 | setState(initialState) 13 | expect(spy).not.toHaveBeenCalled() 14 | }) 15 | 16 | it('should be called if new state identity is different', () => { 17 | const spy = vi.fn() 18 | const initialState = { value: 1, other: 'a' } 19 | const { setState, getState, subscribe } = createStore(() => initialState) 20 | 21 | subscribe(spy) 22 | setState({ ...getState() }) 23 | expect(spy).toHaveBeenCalledWith(initialState, initialState) 24 | }) 25 | 26 | it('should not be called when state slice is the same', () => { 27 | const spy = vi.fn() 28 | const initialState = { value: 1, other: 'a' } 29 | const { setState, subscribe } = createStore( 30 | subscribeWithSelector(() => initialState), 31 | ) 32 | 33 | subscribe((s) => s.value, spy) 34 | setState({ other: 'b' }) 35 | expect(spy).not.toHaveBeenCalled() 36 | }) 37 | 38 | it('should be called when state slice changes', () => { 39 | const spy = vi.fn() 40 | const initialState = { value: 1, other: 'a' } 41 | const { setState, subscribe } = createStore( 42 | subscribeWithSelector(() => initialState), 43 | ) 44 | 45 | subscribe((s) => s.value, spy) 46 | setState({ value: initialState.value + 1 }) 47 | expect(spy).toHaveBeenCalledTimes(1) 48 | expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value) 49 | }) 50 | 51 | it('should not be called when equality checker returns true', () => { 52 | const spy = vi.fn() 53 | const initialState = { value: 1, other: 'a' } 54 | const { setState, subscribe } = createStore( 55 | subscribeWithSelector(() => initialState), 56 | ) 57 | 58 | subscribe((s) => s, spy, { equalityFn: () => true }) 59 | setState({ value: initialState.value + 2 }) 60 | expect(spy).not.toHaveBeenCalled() 61 | }) 62 | 63 | it('should be called when equality checker returns false', () => { 64 | const spy = vi.fn() 65 | const initialState = { value: 1, other: 'a' } 66 | const { setState, subscribe } = createStore( 67 | subscribeWithSelector(() => initialState), 68 | ) 69 | 70 | subscribe((s) => s.value, spy, { equalityFn: () => false }) 71 | setState({ value: initialState.value + 2 }) 72 | expect(spy).toHaveBeenCalledTimes(1) 73 | expect(spy).toHaveBeenCalledWith(initialState.value + 2, initialState.value) 74 | }) 75 | 76 | it('should unsubscribe correctly', () => { 77 | const spy = vi.fn() 78 | const initialState = { value: 1, other: 'a' } 79 | const { setState, subscribe } = createStore( 80 | subscribeWithSelector(() => initialState), 81 | ) 82 | 83 | const unsub = subscribe((s) => s.value, spy) 84 | 85 | setState({ value: initialState.value + 1 }) 86 | unsub() 87 | setState({ value: initialState.value + 2 }) 88 | 89 | expect(spy).toHaveBeenCalledTimes(1) 90 | expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value) 91 | }) 92 | 93 | it('should keep consistent behavior with equality check', () => { 94 | const spy = vi.fn() 95 | const initialState = { value: 1, other: 'a' } 96 | const { getState, setState, subscribe } = createStore( 97 | subscribeWithSelector(() => initialState), 98 | ) 99 | 100 | const isRoughEqual = (x: number, y: number) => Math.abs(x - y) < 1 101 | setState({ value: 0 }) 102 | spy.mockReset() 103 | const spy2 = vi.fn() 104 | let prevValue = getState().value 105 | const unsub = subscribe((s) => { 106 | if (isRoughEqual(prevValue, s.value)) { 107 | // skip assuming values are equal 108 | return 109 | } 110 | spy(s.value, prevValue) 111 | prevValue = s.value 112 | }) 113 | const unsub2 = subscribe((s) => s.value, spy2, { equalityFn: isRoughEqual }) 114 | setState({ value: 0.5 }) 115 | setState({ value: 1 }) 116 | unsub() 117 | unsub2() 118 | expect(spy).toHaveBeenCalledTimes(1) 119 | expect(spy).toHaveBeenCalledWith(1, 0) 120 | expect(spy2).toHaveBeenCalledTimes(1) 121 | expect(spy2).toHaveBeenCalledWith(1, 0) 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "strict": true, 5 | "jsx": "react-jsx", 6 | "esModuleInterop": true, 7 | "module": "esnext", 8 | "moduleResolution": "bundler", 9 | "skipLibCheck": true /* FIXME remove this once vite fixes it */, 10 | "allowImportingTsExtensions": true, 11 | "noUncheckedIndexedAccess": true, 12 | "exactOptionalPropertyTypes": true, 13 | "verbatimModuleSyntax": true, 14 | "declaration": true, 15 | "isolatedDeclarations": true, 16 | "types": ["@testing-library/jest-dom"], 17 | "noEmit": true, 18 | "baseUrl": ".", 19 | "paths": { 20 | "zustand": ["./src/index.ts"], 21 | "zustand/*": ["./src/*.ts"] 22 | } 23 | }, 24 | "include": ["src/**/*", "tests/**/*"], 25 | "exclude": ["node_modules", "dist"] 26 | } 27 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | // eslint-disable-next-line import/extensions 3 | import { defineConfig } from 'vitest/config' 4 | 5 | export default defineConfig({ 6 | resolve: { 7 | alias: [ 8 | { find: /^zustand$/, replacement: resolve('./src/index.ts') }, 9 | { find: /^zustand(.*)$/, replacement: resolve('./src/$1.ts') }, 10 | ], 11 | }, 12 | test: { 13 | name: 'zustand', 14 | // Keeping globals to true triggers React Testing Library's auto cleanup 15 | // https://vitest.dev/guide/migration.html 16 | globals: true, 17 | environment: 'jsdom', 18 | dir: 'tests', 19 | reporters: process.env.GITHUB_ACTIONS 20 | ? ['default', 'github-actions'] 21 | : ['default'], 22 | setupFiles: ['tests/setup.ts'], 23 | coverage: { 24 | include: ['src/**/'], 25 | reporter: ['text', 'json', 'html', 'text-summary'], 26 | reportsDirectory: './coverage/', 27 | provider: 'v8', 28 | }, 29 | }, 30 | }) 31 | --------------------------------------------------------------------------------