Repository: pmndrs/zustand Branch: main Commit: 206012dbd1ae Files: 128 Total size: 680.4 KB Directory structure: gitextract_2ooj7gis/ ├── .codesandbox/ │ └── ci.json ├── .github/ │ ├── DISCUSSION_TEMPLATE/ │ │ └── bug-report.yml │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── config.yml │ ├── dependabot.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/ │ ├── index.md │ ├── learn/ │ │ ├── getting-started/ │ │ │ ├── comparison.md │ │ │ └── introduction.md │ │ ├── guides/ │ │ │ ├── advanced-typescript.md │ │ │ ├── auto-generating-selectors.md │ │ │ ├── beginner-typescript.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 │ │ │ └── updating-state.md │ │ └── index.md │ └── reference/ │ ├── apis/ │ │ ├── create-store.md │ │ ├── create-with-equality-fn.md │ │ ├── create.md │ │ └── shallow.md │ ├── hooks/ │ │ ├── use-shallow.md │ │ ├── use-store-with-equality-fn.md │ │ └── use-store.md │ ├── index.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/ │ │ │ ├── manifest.json │ │ │ └── robots.txt │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── components/ │ │ │ │ ├── CodePreview.jsx │ │ │ │ ├── CopyButton.jsx │ │ │ │ ├── Details.jsx │ │ │ │ ├── Fireflies.jsx │ │ │ │ ├── Scene.jsx │ │ │ │ └── SnippetLang.jsx │ │ │ ├── main.jsx │ │ │ ├── materials/ │ │ │ │ └── layerMaterial.js │ │ │ ├── pmndrs.css │ │ │ ├── resources/ │ │ │ │ ├── javascript-code.js │ │ │ │ └── typescript-code.js │ │ │ ├── styles.css │ │ │ └── utils/ │ │ │ └── copy-to-clipboard.js │ │ └── vite.config.js │ └── starter/ │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── index.css │ │ ├── index.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── pnpm-workspace.yaml ├── rollup.config.mjs ├── src/ │ ├── index.ts │ ├── middleware/ │ │ ├── combine.ts │ │ ├── devtools.ts │ │ ├── immer.ts │ │ ├── persist.ts │ │ ├── redux.ts │ │ ├── ssrSafe.ts │ │ └── subscribeWithSelector.ts │ ├── middleware.ts │ ├── react/ │ │ └── shallow.ts │ ├── react.ts │ ├── shallow.ts │ ├── traditional.ts │ ├── types.d.ts │ ├── vanilla/ │ │ └── shallow.ts │ └── vanilla.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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codesandbox/ci.json ================================================ { "packages": ["dist"], "sandboxes": [ "new", "react-typescript-react-ts", "simple-react-browserify-x9yni", "simple-snowpack-react-o1gmx", "react-parcel-onewf", "next-js-uo1h0", "pavlobu-zustand-demo-frutec" ], "node": "18" } ================================================ FILE: .github/DISCUSSION_TEMPLATE/bug-report.yml ================================================ labels: ['bug'] body: - type: markdown attributes: value: If you don't have a reproduction link, please choose a different category. - type: textarea attributes: label: Bug Description description: Describe the bug you encountered validations: required: true - type: input attributes: label: Reproduction Link 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. validations: required: true ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [dai-shi] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry custom: [ 'https://daishi.gumroad.com/l/uaxms', 'https://daishi.gumroad.com/l/learn-zustand-v4', ] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Assigned issue about: This is to create a new issue that already has an assignee. Please open a new discussion otherwise. title: '' labels: '' assignees: '' --- ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Bug Reports url: https://github.com/pmndrs/zustand/discussions/new?category=bug-report about: Please post bug reports here. - name: Questions url: https://github.com/pmndrs/zustand/discussions/new?category=q-a about: Please post questions here. - name: Other Discussions url: https://github.com/pmndrs/zustand/discussions/new/choose about: Please post ideas and general discussions here. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: 'npm' directory: '/' schedule: interval: 'daily' ignore: - dependency-name: '*' update-types: - 'version-update:semver-patch' - 'version-update:semver-minor' - package-ecosystem: 'github-actions' directory: '/' schedule: interval: 'weekly' ignore: - dependency-name: '*' update-types: - 'version-update:semver-patch' - 'version-update:semver-minor' ================================================ FILE: .github/pull_request_template.md ================================================ ## Related Bug Reports or Discussions Fixes # ## Summary ## Check List - [ ] `pnpm run fix` for formatting and linting code and docs ================================================ FILE: .github/workflows/compressed-size.yml ================================================ name: Compressed Size on: [pull_request] jobs: compressed_size: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - uses: preactjs/compressed-size-action@49c7ff02f46adc39a83c24e91f6110ba8138a19d # v3 with: pattern: './dist/**/*.{js,mjs}' ================================================ FILE: .github/workflows/docs.yml ================================================ name: Build documentation and deploy to GitHub Pages on: push: branches: [main] workflow_dispatch: # Cancel previous run (see: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency) concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: uses: pmndrs/docs/.github/workflows/build.yml@v3 with: mdx: 'docs' libname: 'Zustand' home_redirect: '/learn/getting-started/introduction' icon: '/favicon.ico' logo: '/bear.jpg' github: 'https://github.com/pmndrs/zustand' deploy: needs: build runs-on: ubuntu-latest # Grant GITHUB_TOKEN the permissions required to make a Pages deployment permissions: pages: write # to deploy to Pages id-token: write # to verify the deployment originates from an appropriate source # Deploy to the github-pages environment environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 ================================================ FILE: .github/workflows/preview-release.yml ================================================ name: Preview Release on: [push, pull_request] jobs: preview_release: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - run: pnpm install - run: pnpm run build - run: pnpm dlx pkg-pr-new publish './dist' --compact --template './examples/*' ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish on: release: types: [published] permissions: id-token: write contents: read jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' registry-url: 'https://registry.npmjs.org' cache: 'pnpm' - run: pnpm install - run: pnpm run build - run: npm publish working-directory: dist ================================================ FILE: .github/workflows/test-multiple-builds.yml ================================================ name: Test Multiple Builds on: push: branches: [main] pull_request: types: [opened, synchronize] jobs: test_multiple_builds: runs-on: ubuntu-latest strategy: fail-fast: false matrix: build: [cjs, esm] env: [development] # [development, production] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - run: pnpm install - run: pnpm run build - name: Patch for DEV-ONLY if: ${{ matrix.env == 'development' }} run: | sed -i~ "s/it[.a-zA-Z]*('\[DEV-ONLY\]/it('/" tests/*.tsx sed -i~ "s/it[.a-zA-Z]*('\[PRD-ONLY\]/it.skip('/" tests/*.tsx - name: Patch for PRD-ONLY if: ${{ matrix.env == 'production' }} run: | sed -i~ "s/it[.a-zA-Z]*('\[PRD-ONLY\]/it('/" tests/*.tsx sed -i~ "s/it[.a-zA-Z]*('\[DEV-ONLY\]/it.skip('/" tests/*.tsx - name: Patch for CJS if: ${{ matrix.build == 'cjs' }} run: | sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\1.js')/" vitest.config.mts - name: Patch for ESM if: ${{ matrix.build == 'esm' }} run: | sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\/esm\1.mjs')/" vitest.config.mts sed -i~ "1s/^/import.meta.env.MODE='${NODE_ENV}';/" tests/*.tsx env: NODE_ENV: ${{ matrix.env }} - name: Test ${{ matrix.build }} ${{ matrix.env }} run: | pnpm run test:spec env: NODE_ENV: ${{ matrix.env }} ================================================ FILE: .github/workflows/test-multiple-versions.yml ================================================ name: Test Multiple Versions on: push: branches: [main] pull_request: types: [opened, synchronize] jobs: test_multiple_versions: runs-on: ubuntu-latest strategy: fail-fast: false matrix: react: - 18.0.0 - 18.1.0 - 18.2.0 - 18.3.1 - 19.0.0 - 19.1.0 - 19.2.0 - 19.3.0-canary-da641178-20260129 - 0.0.0-experimental-da641178-20260129 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - run: pnpm install - name: Test ${{ matrix.react }} ${{ matrix.devtools-skip }} run: | pnpm add -D react@${{ matrix.react }} react-dom@${{ matrix.react }} pnpm run test:spec ================================================ FILE: .github/workflows/test-old-typescript.yml ================================================ name: Test Old TypeScript on: push: branches: [main] pull_request: types: [opened, synchronize] jobs: test_old_typescript: runs-on: ubuntu-latest strategy: fail-fast: false matrix: typescript: - 5.9.3 - 5.8.3 - 5.7.3 - 5.6.3 - 5.5.4 - 5.4.5 - 5.3.3 - 5.2.2 - 5.1.6 - 5.0.4 - 4.9.5 - 4.8.4 - 4.7.4 - 4.6.4 - 4.5.5 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - run: pnpm install - run: pnpm run build - name: Patch for all TS run: | sed -i~ 's/"isolatedDeclarations": true,//' tsconfig.json - name: Patch for v4/v3 TS if: ${{ startsWith(matrix.typescript, '4.') || startsWith(matrix.typescript, '3.') }} run: | sed -i~ 's/"verbatimModuleSyntax": true,//' tsconfig.json - name: Patch for Old TS if: ${{ matrix.typescript == '5.3.3' || matrix.typescript == '5.2.2' || matrix.typescript == '5.1.6' || matrix.typescript == '5.0.4' || matrix.typescript == '4.9.5' || matrix.typescript == '4.8.4' || matrix.typescript == '4.7.4' || matrix.typescript == '4.6.4' || matrix.typescript == '4.5.5' }} run: | sed -i~ 's/"moduleResolution": "bundler",/"moduleResolution": "node",/' tsconfig.json sed -i~ 's/"allowImportingTsExtensions": true,//' tsconfig.json sed -i~ 's/"zustand": \["\.\/src\/index\.ts"\],/"zustand": [".\/dist\/index.d.ts"],/' tsconfig.json sed -i~ 's/"zustand\/\*": \["\.\/src\/\*\.ts"\]/"zustand\/*": [".\/dist\/*.d.ts"]/' tsconfig.json sed -i~ 's/"include": .*/"include": ["src\/types.d.ts", "dist\/**\/*", "tests\/**\/*"],/' tsconfig.json - name: Patch for Older TS if: ${{ matrix.typescript == '4.7.4' || matrix.typescript == '4.6.4' || matrix.typescript == '4.5.5' }} run: | pnpm json -I -f package.json -e "this.resolutions={}; this.resolutions['@types/node']='18.13.0';" pnpm add -D @types/node@18.13.0 pnpm add -D vitest@3.2.4 @vitest/coverage-v8@3.2.4 @vitest/ui@3.2.4 - name: Install old TypeScript run: pnpm add -D typescript@${{ matrix.typescript }} - name: Test ${{ matrix.typescript }} run: pnpm run test:types ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: branches: [main] pull_request: types: [opened, synchronize] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - run: pnpm install - run: pnpm run test:format - run: pnpm run test:types - run: pnpm run test:lint - run: pnpm run test:spec - run: pnpm run build # we don't have any other workflows to test build ================================================ FILE: .gitignore ================================================ node_modules/ dist/ Thumbs.db ehthumbs.db Desktop.ini $RECYCLE.BIN/ .DS_Store .vscode .docz/ coverage/ .rpt2_cache/ .idea examples/**/*/package-lock.json examples/**/*/yarn.lock examples/**/*/pnpm-lock.yaml examples/**/*/bun.lockb ================================================ FILE: .prettierignore ================================================ dist pnpm-lock.yaml ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing ## General Guideline ### Reporting Issues If you have found what you think is a bug, please [start a discussion](https://github.com/pmndrs/zustand/discussions/new?category=bug-report). For any usage questions, please [start a discussion](https://github.com/pmndrs/zustand/discussions/new?category=q-a). ### Suggesting New Features 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. ### Committing 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: Your commit type must be one of the following: - **feat**: A new feature. - **fix**: A bug fix. - **refactor**: A code change that neither fixes a bug nor adds a feature. - **chore**: Changes to the build process, configuration, dependencies, CI/CD pipelines, or other auxiliary tools and libraries. - **docs**: Documentation-only changes. - **test**: Adding missing or correcting existing tests. If you are unfamiliar with the usage of conventional commits, the short version is to simply specify the type as a first word, and follow it with a colon and a space, then start your message from a lowercase letter, like this: ``` feat: add a 'foo' type support ``` You can also specify the scope of the commit in the parentheses after a type: ``` fix(react): change the 'bar' parameter type ``` ### Development If you would like to contribute by fixing an open issue or developing a new feature you can use this suggested workflow: #### General 1. Fork this repository. 2. Create a new feature branch based off the `main` branch. 3. Follow the [Core](#Core) and/or the [Documentation](#Documentation) guide below and come back to this once done. 4. Run `pnpm run fix:format` to format the code. 5. Git stage your required changes and commit (review the commit guidelines below). 6. Submit the PR for review. ##### Core 1. Run `pnpm install` to install dependencies. 2. Create failing tests for your fix or new feature in the [`tests`](./tests/) folder. 3. Implement your changes. 4. Run `pnpm run build` to build the library. _(Pro-tip: `pnpm run build-watch` runs the build in watch mode)_ 5. Run the tests by running `pnpm run test` and ensure that they pass. 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) 7. Follow step 4 and onwards from the [General](#General) guide above to bring it to the finish line. ### Pull Requests Please try to keep your pull request focused in scope and avoid including unrelated commits. 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. ## Zustand-specific Guideline ##### Documentation Our [docs](https://zustand.docs.pmnd.rs) are based on [`pmndrs/docs`](https://github.com/pmndrs/docs). 1. Separately, clone the `pmndrs/docs`. (you don't need to fork it). 2. Inside the `pmndrs/docs` directory: 1. Create a `.env` file in the root directory with the next environment variables: `MDX=docs/zustand/docs` and `HOME_REDIRECT=/getting-started/introduction`. 2. Run `npm install` to install dependencies. 3. Run `npm run dev` to start the dev server. 4. Navigate to [`http://localhost:3000`](http://localhost:3000) to view the documents. 3. Go Back to the forked repository: 1. Run `pnpm install` to install dependencies. 2. Navigate to the [`docs`](./docs/) folder and make necessary changes to the documents. 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 `pmndrs/docs` repository) 4. Follow step 4 and onwards from the [General](#General) guide above to bring it to the finish line. Thank you for contributing! :heart: ================================================ FILE: FUNDING.json ================================================ { "drips": { "ethereum": { "ownedBy": "0xBA918e34bed77Ba7a9fCF53be0A81FA538d56FA7" } } } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Paul Henschel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

[![Build Status](https://img.shields.io/github/actions/workflow/status/pmndrs/zustand/test.yml?branch=main&style=flat&colorA=000000&colorB=000000)](https://github.com/pmndrs/zustand/actions?query=workflow%3ATest) [![Build Size](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdeno.bundlejs.com%2F%3Fq%3Dzustand&query=%24.size.uncompressedSize&style=flat&label=bundle%20size&colorA=000000&colorB=000000)](https://bundlejs.com/?q=zustand) [![Version](https://img.shields.io/npm/v/zustand?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/zustand) [![Downloads](https://img.shields.io/npm/dt/zustand.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/zustand) [![Discord Shield](https://img.shields.io/discord/740090768164651008?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=ffffff)](https://discord.gg/poimandres)

A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy API based on hooks, isn't boilerplatey or opinionated. Don't disregard it because it's cute. It has quite the claws, lots of time was spent dealing with common pitfalls, like the dreaded [zombie child problem](https://react-redux.js.org/api/hooks#stale-props-and-zombie-children), [react concurrency](https://github.com/bvaughn/rfcs/blob/useMutableSource/text/0000-use-mutable-source.md), and [context loss](https://github.com/facebook/react/issues/13332) between mixed renderers. It may be the one state-manager in the React space that gets all of these right. You can try a live [demo](https://zustand-demo.pmnd.rs/) and read the [docs](https://zustand.docs.pmnd.rs/). ```bash npm install zustand ``` :warning: This readme is written for JavaScript users. If you are a TypeScript user, be sure to check out our [TypeScript Usage section](#typescript-usage). ## First create a store Your store is a hook! You can put anything in it: primitives, objects, functions. State has to be updated immutably and the `set` function [merges state](./docs/guides/immutable-state-and-merging.md) to help it. ```jsx import { create } from 'zustand' const useBearStore = create((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })) ``` ## Then bind your components, and that's it! Use the hook anywhere, no providers are needed. Select your state and the component will re-render on changes. ```jsx function BearCounter() { const bears = useBearStore((state) => state.bears) return

{bears} around here ...

} function Controls() { const increasePopulation = useBearStore((state) => state.increasePopulation) return } ``` ### Why zustand over redux? - Simple and un-opinionated - Makes hooks the primary means of consuming state - Doesn't wrap your app in context providers - [Can inform components transiently (without causing render)](#transient-updates-for-often-occurring-state-changes) ### Why zustand over context? - Less boilerplate - Renders components only on changes - Centralized, action-based state management --- # Recipes ## Fetching everything You can, but bear in mind that it will cause the component to update on every state change! ```jsx const state = useBearStore() ``` ## Selecting multiple state slices It detects changes with strict-equality (old === new) by default, this is efficient for atomic state picks. ```jsx const nuts = useBearStore((state) => state.nuts) const honey = useBearStore((state) => state.honey) ``` If you want to construct a single object with multiple state-picks inside, similar to redux's mapStateToProps, you can use [useShallow](./docs/guides/prevent-rerenders-with-use-shallow.md) to prevent unnecessary rerenders when the selector output does not change according to shallow equal. ```jsx import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' const useBearStore = create((set) => ({ nuts: 0, honey: 0, treats: {}, // ... })) // Object pick, re-renders the component when either state.nuts or state.honey change const { nuts, honey } = useBearStore( useShallow((state) => ({ nuts: state.nuts, honey: state.honey })), ) // Array pick, re-renders the component when either state.nuts or state.honey change const [nuts, honey] = useBearStore( useShallow((state) => [state.nuts, state.honey]), ) // Mapped picks, re-renders the component when state.treats changes in order, count or keys const treats = useBearStore(useShallow((state) => Object.keys(state.treats))) ``` For more control over re-rendering, you may provide any custom equality function (this example requires the use of [`createWithEqualityFn`](./docs/migrations/migrating-to-v5.md#using-custom-equality-functions-such-as-shallow)). ```jsx const treats = useBearStore( (state) => state.treats, (oldTreats, newTreats) => compare(oldTreats, newTreats), ) ``` ## Overwriting state The `set` function has a second argument, `false` by default. Instead of merging, it will replace the state model. Be careful not to wipe out parts you rely on, like actions. ```jsx const useFishStore = create((set) => ({ salmon: 1, tuna: 2, deleteEverything: () => set({}, true), // clears the entire store, actions included deleteTuna: () => set(({ tuna, ...rest }) => rest, true), })) ``` ## Async actions Just call `set` when you're ready, zustand doesn't care if your actions are async or not. ```jsx const useFishStore = create((set) => ({ fishies: {}, fetch: async (pond) => { const response = await fetch(pond) set({ fishies: await response.json() }) }, })) ``` ## Read from state in actions `set` allows fn-updates `set(state => result)`, but you still have access to state outside of it through `get`. ```jsx const useSoundStore = create((set, get) => ({ sound: 'grunt', action: () => { const sound = get().sound ... ``` ## Reading/writing state and reacting to changes outside of components Sometimes you need to access state in a non-reactive way or act upon the store. For these cases, the resulting hook has utility functions attached to its prototype. :warning: This technique is not recommended for adding state in [React Server Components](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md) (typically in Next.js 13 and above). It can lead to unexpected bugs and privacy issues for your users. For more details, see [#2200](https://github.com/pmndrs/zustand/discussions/2200). ```jsx const useDogStore = create(() => ({ paw: true, snout: true, fur: true })) // Getting non-reactive fresh state const paw = useDogStore.getState().paw // Listening to all changes, fires synchronously on every change const unsub1 = useDogStore.subscribe(console.log) // Updating state, will trigger listeners useDogStore.setState({ paw: false }) // Unsubscribe listeners unsub1() // You can of course use the hook as you always would function Component() { const paw = useDogStore((state) => state.paw) ... ``` ### Using subscribe with selector If you need to subscribe with a selector, `subscribeWithSelector` middleware will help. With this middleware `subscribe` accepts an additional signature: ```ts subscribe(selector, callback, options?: { equalityFn, fireImmediately }): Unsubscribe ``` ```js import { subscribeWithSelector } from 'zustand/middleware' const useDogStore = create( subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })), ) // Listening to selected changes, in this case when "paw" changes const unsub2 = useDogStore.subscribe((state) => state.paw, console.log) // Subscribe also exposes the previous value const unsub3 = useDogStore.subscribe( (state) => state.paw, (paw, previousPaw) => console.log(paw, previousPaw), ) // Subscribe also supports an optional equality function const unsub4 = useDogStore.subscribe( (state) => [state.paw, state.fur], console.log, { equalityFn: shallow }, ) // Subscribe and fire immediately const unsub5 = useDogStore.subscribe((state) => state.paw, console.log, { fireImmediately: true, }) ``` ## Using zustand without React Zustand core can be imported and used without the React dependency. The only difference is that the create function does not return a hook, but the API utilities. ```jsx import { createStore } from 'zustand/vanilla' const store = createStore((set) => ...) const { getState, setState, subscribe, getInitialState } = store export default store ``` You can use a vanilla store with `useStore` hook available since v4. ```jsx import { useStore } from 'zustand' import { vanillaStore } from './vanillaStore' const useBoundStore = (selector) => useStore(vanillaStore, selector) ``` :warning: Note that middlewares that modify `set` or `get` are not applied to `getState` and `setState`. ## Transient updates (for often occurring state-changes) The subscribe function allows components to bind to a state-portion without forcing re-render on changes. Best combine it with useEffect for automatic unsubscribe on unmount. This can make a [drastic](https://codesandbox.io/s/peaceful-johnson-txtws) performance impact when you are allowed to mutate the view directly. ```jsx const useScratchStore = create((set) => ({ scratches: 0, ... })) const Component = () => { // Fetch initial state const scratchRef = useRef(useScratchStore.getState().scratches) // Connect to the store on mount, disconnect on unmount, catch state-changes in a reference useEffect(() => useScratchStore.subscribe( state => (scratchRef.current = state.scratches) ), []) ... ``` ## Sick of reducers and changing nested states? Use Immer! Reducing nested structures is tiresome. Have you tried [immer](https://github.com/mweststrate/immer)? ```jsx import { produce } from 'immer' const useLushStore = create((set) => ({ lush: { forest: { contains: { a: 'bear' } } }, clearForest: () => set( produce((state) => { state.lush.forest.contains = null }), ), })) const clearForest = useLushStore((state) => state.clearForest) clearForest() ``` [Alternatively, there are some other solutions.](./docs/guides/updating-state.md#with-immer) ## Persist middleware You can persist your store's data using any kind of storage. ```jsx import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' const useFishStore = create( persist( (set, get) => ({ fishes: 0, addAFish: () => set({ fishes: get().fishes + 1 }), }), { name: 'food-storage', // name of the item in the storage (must be unique) storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used }, ), ) ``` [See the full documentation for this middleware.](./docs/reference/integrations/persisting-store-data.md) ## Immer middleware Immer is available as middleware too. ```jsx import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' const useBeeStore = create( immer((set) => ({ bees: 0, addBees: (by) => set((state) => { state.bees += by }), })), ) ``` ## Can't live without redux-like reducers and action types? ```jsx const types = { increase: 'INCREASE', decrease: 'DECREASE' } const reducer = (state, { type, by = 1 }) => { switch (type) { case types.increase: return { grumpiness: state.grumpiness + by } case types.decrease: return { grumpiness: state.grumpiness - by } } } const useGrumpyStore = create((set) => ({ grumpiness: 0, dispatch: (args) => set((state) => reducer(state, args)), })) const dispatch = useGrumpyStore((state) => state.dispatch) dispatch({ type: types.increase, by: 2 }) ``` Or, just use our redux-middleware. It wires up your main-reducer, sets the initial state, and adds a dispatch function to the state itself and the vanilla API. ```jsx import { redux } from 'zustand/middleware' const useGrumpyStore = create(redux(reducer, initialState)) ``` ## Redux devtools Install the [Redux DevTools Chrome extension](https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) to use the devtools middleware. ```jsx import { devtools } from 'zustand/middleware' // Usage with a plain action store, it will log actions as "setState" const usePlainStore = create(devtools((set) => ...)) // Usage with a redux store, it will log full action types const useReduxStore = create(devtools(redux(reducer, initialState))) ``` One redux devtools connection for multiple stores ```jsx import { devtools } from 'zustand/middleware' // Usage with a plain action store, it will log actions as "setState" const usePlainStore1 = create(devtools((set) => ..., { name, store: storeName1 })) const usePlainStore2 = create(devtools((set) => ..., { name, store: storeName2 })) // Usage with a redux store, it will log full action types const useReduxStore1 = create(devtools(redux(reducer, initialState)), { name, store: storeName3 }) const useReduxStore2 = create(devtools(redux(reducer, initialState)), { name, store: storeName4 }) ``` Assigning different connection names will separate stores in redux devtools. This also helps group different stores into separate redux devtools connections. devtools takes the store function as its first argument, optionally you can name the store or configure [serialize](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#serialize) options with a second argument. Name store: `devtools(..., {name: "MyStore"})`, which will create a separate instance named "MyStore" in the devtools. Serialize options: `devtools(..., { serialize: { options: true } })`. #### Logging Actions devtools will only log actions from each separated store unlike in a typical _combined reducers_ redux store. See an approach to combining stores https://github.com/pmndrs/zustand/issues/163 You can log a specific action type for each `set` function by passing a third parameter: ```jsx const useBearStore = create(devtools((set) => ({ ... eatFish: () => set( (prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 }), undefined, 'bear/eatFish' ), ... ``` You can also log the action's type along with its payload: ```jsx ... addFishes: (count) => set( (prev) => ({ fishes: prev.fishes + count }), undefined, { type: 'bear/addFishes', count, } ), ... ``` If an action type is not provided, it is defaulted to "anonymous". You can customize this default value by providing an `anonymousActionType` parameter: ```jsx devtools(..., { anonymousActionType: 'unknown', ... }) ``` If you wish to disable devtools (on production for instance). You can customize this setting by providing the `enabled` parameter: ```jsx devtools(..., { enabled: false, ... }) ``` ## React context The store created with `create` doesn't require context providers. In some cases, you may want to use contexts for dependency injection or if you want to initialize your store with props from a component. Because the normal store is a hook, passing it as a normal context value may violate the rules of hooks. The recommended method available since v4 is to use the vanilla store. ```jsx import { createContext, useContext } from 'react' import { createStore, useStore } from 'zustand' const store = createStore(...) // vanilla store without hooks const StoreContext = createContext() const App = () => ( ... ) const Component = () => { const store = useContext(StoreContext) const slice = useStore(store, selector) ... ``` ## TypeScript Usage Basic typescript usage doesn't require anything special except for writing `create()(...)` instead of `create(...)`... ```ts import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' import type {} from '@redux-devtools/extension' // required for devtools typing interface BearState { bears: number increase: (by: number) => void } const useBearStore = create()( devtools( persist( (set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), }), { name: 'bear-storage', }, ), ), ) ``` A more detailed TypeScript guide is [here](docs/guides/beginner-typescript.md) and [there](docs/guides/advanced-typescript.md). ## Best practices - You may wonder how to organize your code for better maintenance: [Splitting the store into separate slices](./docs/learn/guides/slices-pattern.md). - Recommended usage for this unopinionated library: [Flux inspired practice](./docs/learn/guides/flux-inspired-practice.md). - [Calling actions outside a React event handler in pre-React 18](./docs/learn/guides/event-handler-in-pre-react-18.md). - [Testing](./docs/learn/guides/testing.md) - For more, have a look [in the docs folder](./docs/index.md) ## Third-Party Libraries Some users may want to extend Zustand's feature set which can be done using third-party libraries made by the community. For information regarding third-party libraries with Zustand, visit [the doc](./docs/reference/integrations/third-party-libraries.md). ## Comparison with other libraries - [Difference between zustand and other state management libraries for React](https://zustand.docs.pmnd.rs/learn/getting-started/comparison) ================================================ FILE: docs/index.md ================================================ --- pageType: home hero: text: Bear necessities for React state tagline: A tiny, predictable store with hooks-first ergonomics and escape hatches that stay out of your way. actions: - theme: brand text: Introduction link: ./learn/getting-started/introduction.md - theme: alt text: Quick Start link: ./learn/index.md features: - title: Minimal API, fast adoption details: Create a store with a single hook, subscribe with selectors, and avoid boilerplate or providers. - title: Safe under React concurrency details: Built to avoid zombie children and tearing issues while keeping renders predictable. - title: Works across React and vanilla details: Share stores between React, React Native, and non-React environments with the same API surface. - title: Batteries included details: Opt into devtools, persistence, Redux-style middleware, and Immer without changing your mental model. - title: TypeScript-first ergonomics details: Strongly typed helpers and patterns so your state and actions stay inferred. - title: Small footprint details: Tiny bundle size with zero config and performance that keeps pace in production. --- ================================================ FILE: docs/learn/getting-started/comparison.md ================================================ --- title: Comparison description: How Zustand stacks up against similar libraries nav: 2 --- Zustand is one of many state management libraries for React. On this page we will discuss Zustand in comparison to some of these libraries, including Redux, Valtio, Jotai, and Recoil. Each library has its own strengths and weaknesses, and we will compare key differences and similarities between each. ## Redux ### State Model (vs Redux) Conceptually, Zustand and Redux are quite similar, both are based on an immutable state model. However, Redux requires your app to be wrapped in context providers; Zustand does not. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } type Actions = { increment: (qty: number) => void decrement: (qty: number) => void } const useCountStore = create((set) => ({ count: 0, increment: (qty: number) => set((state) => ({ count: state.count + qty })), decrement: (qty: number) => set((state) => ({ count: state.count - qty })), })) ``` ```ts import { create } from 'zustand' type State = { count: number } type Action = { type: 'increment' | 'decrement' qty: number } type Actions = { dispatch: (action: Action) => void } const countReducer = (state: State, action: Action) => { switch (action.type) { case 'increment': return { count: state.count + action.qty } case 'decrement': return { count: state.count - action.qty } default: return state } } const useCountStore = create((set) => ({ count: 0, dispatch: (action: Action) => set((state) => countReducer(state, action)), })) ``` **Redux** ```ts import { createStore } from 'redux' import { useSelector, useDispatch } from 'react-redux' type State = { count: number } type Action = { type: 'increment' | 'decrement' qty: number } const countReducer = (state: State, action: Action) => { switch (action.type) { case 'increment': return { count: state.count + action.qty } case 'decrement': return { count: state.count - action.qty } default: return state } } const countStore = createStore(countReducer) ``` ```ts import { createSlice, configureStore } from '@reduxjs/toolkit' const countSlice = createSlice({ name: 'count', initialState: { value: 0 }, reducers: { incremented: (state, qty: number) => { // Redux Toolkit does not mutate the state, it uses the Immer library // behind scenes, allowing us to have something called "draft state". state.value += qty }, decremented: (state, qty: number) => { state.value -= qty }, }, }) const countStore = configureStore({ reducer: countSlice.reducer }) ``` ### Render Optimization (vs Redux) When it comes to render optimizations within your app, there are no major differences in approach between Zustand and Redux. In both libraries it is recommended that you manually apply render optimizations by using selectors. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } type Actions = { increment: (qty: number) => void decrement: (qty: number) => void } const useCountStore = create((set) => ({ count: 0, increment: (qty: number) => set((state) => ({ count: state.count + qty })), decrement: (qty: number) => set((state) => ({ count: state.count - qty })), })) const Component = () => { const count = useCountStore((state) => state.count) const increment = useCountStore((state) => state.increment) const decrement = useCountStore((state) => state.decrement) // ... } ``` **Redux** ```ts import { createStore } from 'redux' import { useSelector, useDispatch } from 'react-redux' type State = { count: number } type Action = { type: 'increment' | 'decrement' qty: number } const countReducer = (state: State, action: Action) => { switch (action.type) { case 'increment': return { count: state.count + action.qty } case 'decrement': return { count: state.count - action.qty } default: return state } } const countStore = createStore(countReducer) const Component = () => { const count = useSelector((state) => state.count) const dispatch = useDispatch() // ... } ``` ```ts import { useSelector } from 'react-redux' import type { TypedUseSelectorHook } from 'react-redux' import { createSlice, configureStore } from '@reduxjs/toolkit' const countSlice = createSlice({ name: 'count', initialState: { value: 0 }, reducers: { incremented: (state, qty: number) => { // Redux Toolkit does not mutate the state, it uses the Immer library // behind scenes, allowing us to have something called "draft state". state.value += qty }, decremented: (state, qty: number) => { state.value -= qty }, }, }) const countStore = configureStore({ reducer: countSlice.reducer }) const useAppSelector: TypedUseSelectorHook = useSelector const useAppDispatch: () => typeof countStore.dispatch = useDispatch const Component = () => { const count = useAppSelector((state) => state.count.value) const dispatch = useAppDispatch() // ... } ``` ## Valtio ### State Model (vs Valtio) Zustand and Valtio approach state management in a fundamentally different way. Zustand is based on the **immutable** state model, while Valtio is based on the **mutable** state model. **Zustand** ```ts import { create } from 'zustand' type State = { obj: { count: number } } const store = create(() => ({ obj: { count: 0 } })) store.setState((prev) => ({ obj: { count: prev.obj.count + 1 } })) ``` **Valtio** ```ts import { proxy } from 'valtio' const state = proxy({ obj: { count: 0 } }) state.obj.count += 1 ``` ### Render Optimization (vs Valtio) The other difference between Zustand and Valtio is Valtio makes render optimizations through property access. However, with Zustand, it is recommended that you manually apply render optimizations by using selectors. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } const useCountStore = create(() => ({ count: 0, })) const Component = () => { const count = useCountStore((state) => state.count) // ... } ``` **Valtio** ```ts import { proxy, useSnapshot } from 'valtio' const state = proxy({ count: 0, }) const Component = () => { const { count } = useSnapshot(state) // ... } ``` ## Jotai ### State Model (vs Jotai) There is one major difference between Zustand and Jotai. Zustand is a single store, while Jotai consists of primitive atoms that can be composed together. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } type Actions = { updateCount: ( countCallback: (count: State['count']) => State['count'], ) => void } const useCountStore = create((set) => ({ count: 0, updateCount: (countCallback) => set((state) => ({ count: countCallback(state.count) })), })) ``` **Jotai** ```ts import { atom } from 'jotai' const countAtom = atom(0) ``` ### Render Optimization (vs Jotai) Jotai achieves render optimizations through atom dependency. However, with Zustand it is recommended that you manually apply render optimizations by using selectors. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } type Actions = { updateCount: ( countCallback: (count: State['count']) => State['count'], ) => void } const useCountStore = create((set) => ({ count: 0, updateCount: (countCallback) => set((state) => ({ count: countCallback(state.count) })), })) const Component = () => { const count = useCountStore((state) => state.count) const updateCount = useCountStore((state) => state.updateCount) // ... } ``` **Jotai** ```ts import { atom, useAtom } from 'jotai' const countAtom = atom(0) const Component = () => { const [count, updateCount] = useAtom(countAtom) // ... } ``` ## Recoil ### State Model (vs Recoil) The difference between Zustand and Recoil is similar to that between Zustand and Jotai. Recoil depends on atom string keys instead of atom object referential identities. Additionally, Recoil needs to wrap your app in a context provider. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } type Actions = { setCount: (countCallback: (count: State['count']) => State['count']) => void } const useCountStore = create((set) => ({ count: 0, setCount: (countCallback) => set((state) => ({ count: countCallback(state.count) })), })) ``` **Recoil** ```ts import { atom } from 'recoil' const count = atom({ key: 'count', default: 0, }) ``` ### Render Optimization (vs Recoil) Similar to previous optimization comparisons, Recoil makes render optimizations through atom dependency. Whereas with Zustand, it is recommended that you manually apply render optimizations by using selectors. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } type Actions = { setCount: (countCallback: (count: State['count']) => State['count']) => void } const useCountStore = create((set) => ({ count: 0, setCount: (countCallback) => set((state) => ({ count: countCallback(state.count) })), })) const Component = () => { const count = useCountStore((state) => state.count) const setCount = useCountStore((state) => state.setCount) // ... } ``` **Recoil** ```ts import { atom, useRecoilState } from 'recoil' const countAtom = atom({ key: 'count', default: 0, }) const Component = () => { const [count, setCount] = useRecoilState(countAtom) // ... } ``` ## Npm Downloads Trend - [Npm Downloads Trend of State Management Libraries for React](https://npm-stat.com/charts.html?package=zustand&package=jotai&package=valtio&package=%40reduxjs%2Ftoolkit&package=recoil) ================================================ FILE: docs/learn/getting-started/introduction.md ================================================ --- title: Introduction description: How to use Zustand nav: 1 ---
Logo Zustand
A small, fast, and scalable bearbones state management solution. Zustand has a comfy API based on hooks. It isn't boilerplatey or opinionated, but has enough convention to be explicit and flux-like. Don't disregard it because it's cute, it has claws! Lots of time was spent to deal with common pitfalls, like the dreaded [zombie child problem], [React concurrency], and [context loss] between mixed renderers. It may be the one state manager in the React space that gets all of these right. You can try a live demo [here](https://codesandbox.io/s/dazzling-moon-itop4). [zombie child problem]: https://react-redux.js.org/api/hooks#stale-props-and-zombie-children [react concurrency]: https://github.com/bvaughn/rfcs/blob/useMutableSource/text/0000-use-mutable-source.md [context loss]: https://github.com/facebook/react/issues/13332 ## Installation Zustand is available as a package on NPM for use: ```bash # NPM npm install zustand # Or, use any package manager of your choice. ``` ## First create a store Your store is a hook! You can put anything in it: primitives, objects, functions. The `set` function _merges_ state. ```js import { create } from 'zustand' const useBear = create((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), updateBears: (newBears) => set({ bears: newBears }), })) ``` ## Then bind your components, and that's it! You can use the hook anywhere, without the need of providers. Select your state and the consuming component will re-render when that state changes. ```jsx function BearCounter() { const bears = useBear((state) => state.bears) return

{bears} bears around here...

} function Controls() { const increasePopulation = useBear((state) => state.increasePopulation) return } ``` ================================================ FILE: docs/learn/guides/advanced-typescript.md ================================================ --- title: Advanced TypeScript Guide nav: 13 --- ## Basic usage The difference when using TypeScript is that instead of writing `create(...)`, you have to write `create()(...)` (notice the extra parentheses `()` too along with the type parameter) where `T` is the type of the state to annotate it. For example: ```ts import { create } from 'zustand' interface BearState { bears: number increase: (by: number) => void } const useBearStore = create()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) ```
Why can't we simply infer the type from the initial state?
**TLDR**: Because state generic `T` is invariant. Consider this minimal version `create`: ```ts declare const create: (f: (get: () => T) => T) => T const x = create((get) => ({ foo: 0, bar: () => get(), })) // `x` is inferred as `unknown` instead of // interface X { // foo: number, // bar: () => X // } ``` Here, if you look at the type of `f` in `create`, i.e. `(get: () => T) => T`, it "gives" `T` via return (making it covariant), but it also "takes" `T` via `get` (making it contravariant). "So where does `T` come from?" TypeScript wonders. It's like that chicken or egg problem. At the end TypeScript, gives up and infers `T` as `unknown`. So, as long as the generic to be inferred is invariant (i.e. both covariant and contravariant), TypeScript will be unable to infer it. Another simple example would be this: ```ts const createFoo = {} as (f: (t: T) => T) => T const x = createFoo((_) => 'hello') ``` Here again, `x` is `unknown` instead of `string`.
More about the inference (just for the people curious and interested in TypeScript) In some sense this inference failure is not a problem because a value of type `(f: (t: T) => T) => T` cannot be written. That is to say you can't write the real runtime implementation of `createFoo`. Let's try it: ```js const createFoo = (f) => f(/* ? */) ``` `createFoo` needs to return the return value of `f`. And to do that we first have to call `f`. And to call it we have to pass a value of type `T`. And to pass a value of type `T` we first have to produce it. But how can we produce a value of type `T` when we don't even know what `T` is? The only way to produce a value of type `T` is to call `f`, but then to call `f` itself we need a value of type `T`. So you see it's impossible to actually write `createFoo`. So what we're saying is, the inference failure in case of `createFoo` is not really a problem because it's impossible to implement `createFoo`. But what about the inference failure in case of `create`? That also is not really a problem because it's impossible to implement `create` too. Wait a minute, if it's impossible to implement `create` then how does Zustand implement it? The answer is, it doesn't. Zustand lies that it implemented `create`'s type, it implemented only the most part of it. Here's a simple proof by showing unsoundness. Consider the following code: ```ts import { create } from 'zustand' const useBoundStore = create<{ foo: number }>()((_, get) => ({ foo: get().foo, })) ``` This code compiles. But if we run it, we'll get an exception: "Uncaught TypeError: Cannot read properties of undefined (reading 'foo')". This is because `get` would return `undefined` before the initial state is created (hence you shouldn't call `get` when creating the initial state). The types promise that `get` will never return `undefined` but it does initially, which means Zustand failed to implement it. And of course Zustand failed because it's impossible to implement `create` the way types promise (in the same way it's impossible to implement `createFoo`). In other words we don't have a type to express the actual `create` we have implemented. We can't type `get` as `() => T | undefined` because it would cause inconvenience and it still won't be correct as `get` is indeed `() => T` eventually, just if called synchronously it would be `() => undefined`. What we need is some kind of TypeScript feature that allows us to type `get` as `(() => T) & WhenSync<() => undefined>`, which of course is extremely far-fetched. So we have two problems: lack of inference and unsoundness. Lack of inference can be solved if TypeScript can improve its inference for invariants. And unsoundness can be solved if TypeScript introduces something like `WhenSync`. To work around lack of inference we manually annotate the state type. And we can't work around unsoundness, but it's not a big deal because it's not much, calling `get` synchronously anyway doesn't make sense.
Why the currying `()(...)`?
**TLDR**: It is a workaround for [microsoft/TypeScript#10571](https://github.com/microsoft/TypeScript/issues/10571). Imagine you have a scenario like this: ```ts declare const withError: ( p: Promise, ) => Promise<[error: undefined, value: T] | [error: E, value: undefined]> declare const doSomething: () => Promise const main = async () => { let [error, value] = await withError(doSomething()) } ``` Here, `T` is inferred to be a `string` and `E` is inferred to be `unknown`. You might want to annotate `E` as `Foo`, because you are certain of the shape of error `doSomething()` would throw. However, you can't do that. You can either pass all generics or none. Along with annotating `E` as `Foo`, you will also have to annotate `T` as `string` even though it gets inferred anyway. The solution is to make a curried version of `withError` that does nothing at runtime. Its purpose is to just allow you annotate `E`. ```ts declare const withError: { (): ( p: Promise, ) => Promise<[error: undefined, value: T] | [error: E, value: undefined]> ( p: Promise, ): Promise<[error: undefined, value: T] | [error: E, value: undefined]> } declare const doSomething: () => Promise interface Foo { bar: string } const main = async () => { let [error, value] = await withError()(doSomething()) } ``` This way, `T` gets inferred and you get to annotate `E`. Zustand has the same use case when we want to annotate the state (the first type parameter) but allow other parameters to get inferred.
Alternatively, you can also use `combine`, which infers the state so that you do not need to type it. ```ts import { create } from 'zustand' import { combine } from 'zustand/middleware' const useBearStore = create( combine({ bears: 0 }, (set) => ({ increase: (by: number) => set((state) => ({ bears: state.bears + by })), })), ) ```
Be a little careful
We achieve the inference by lying a little in the types of `set`, `get`, and `store` that you receive as parameters. The lie is that they're typed as if the state is the first parameter, when in fact the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. For example, `get` from the second parameter has type `() => { bears: number }` and that is a lie as it should be `() => { bears: number, increase: (by: number) => void }`. And `useBearStore` still has the correct type; for example, `useBearStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`. It isn't really a lie because `{ bears: number }` is still a subtype of `{ bears: number, increase: (by: number) => void }`. Therefore, there will be no problem in most cases. You should just be careful while using replace. For example, `set({ bears: 0 }, true)` would compile but will be unsound as it will delete the `increase` function. Another instance where you should be careful is if you use `Object.keys`. `Object.keys(get())` will return `["bears", "increase"]` and not `["bears"]`. The return type of `get` can make you fall for these mistakes. `combine` trades off a little type-safety for the convenience of not having to write a type for state. Hence, you should use `combine` accordingly. It is fine in most cases and you can use it conveniently.
Note that we don't use the curried version when using `combine` because `combine` "creates" the state. When using a middleware that creates the state, it isn't necessary to use the curried version because the state now can be inferred. Another middleware that creates state is `redux`. So when using `combine`, `redux`, or any other custom middleware that creates the state, we don't recommend using the curried version. If you want to infer state type also outside of state declaration, you can use the `ExtractState` type helper: ```ts import { create, ExtractState } from 'zustand' import { combine } from 'zustand/middleware' type BearState = ExtractState const useBearStore = create( combine({ bears: 0 }, (set) => ({ increase: (by: number) => set((state) => ({ bears: state.bears + by })), })), ) ``` ## Using middlewares You do not have to do anything special to use middlewares in TypeScript. ```ts import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' interface BearState { bears: number increase: (by: number) => void } const useBearStore = create()( devtools( persist( (set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), }), { name: 'bearStore' }, ), ), ) ``` Just make sure you are using them immediately inside `create` so as to make the contextual inference work. Doing something even remotely fancy like the following `myMiddlewares` would require more advanced types. ```ts import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' const myMiddlewares = (f) => devtools(persist(f, { name: 'bearStore' })) interface BearState { bears: number increase: (by: number) => void } const useBearStore = create()( myMiddlewares((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })), ) ``` Also, we recommend using `devtools` middleware as last as possible. For example, when you use it with `immer` as a middleware, it should be `devtools(immer(...))` and not `immer(devtools(...))`. This is because`devtools` mutates the `setState` and adds a type parameter on it, which could get lost if other middlewares (like `immer`) also mutate `setState` before `devtools`. Hence using `devtools` at the end makes sure that no middlewares mutate `setState` before it. ## Authoring middlewares and advanced usage Imagine you had to write this hypothetical middleware. ```ts import { create } from 'zustand' const foo = (f, bar) => (set, get, store) => { store.foo = bar return f(set, get, store) } const useBearStore = create(foo(() => ({ bears: 0 }), 'hello')) console.log(useBearStore.foo.toUpperCase()) ``` Zustand middlewares can mutate the store. But how could we possibly encode the mutation on the type-level? That is to say how could we type `foo` so that this code compiles? For a usual statically typed language, this is impossible. But thanks to TypeScript, Zustand has something called a "higher-kinded mutator" that makes this possible. If you are dealing with complex type problems, like typing a middleware or using the `StateCreator` type, you will have to understand this implementation detail. For this, you can [check out #710](https://github.com/pmndrs/zustand/issues/710). If you are eager to know what the answer is to this particular problem then you can [see it here](#middleware-that-changes-the-store-type). ### Handling Dynamic `replace` Flag If the value of the `replace` flag is not known at compile time and is determined dynamically, you might face issues. To handle this, you can use a workaround by annotating the `replace` parameter with the parameters of the `setState` function: ```ts const replaceFlag = Math.random() > 0.5 const args = [{ bears: 5 }, replaceFlag] as Parameters< typeof useBearStore.setState > store.setState(...args) ``` #### Example with `as Parameters` Workaround ```ts import { create } from 'zustand' interface BearState { bears: number increase: (by: number) => void } const useBearStore = create()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) const replaceFlag = Math.random() > 0.5 const args = [{ bears: 5 }, replaceFlag] as Parameters< typeof useBearStore.setState > useBearStore.setState(...args) // Using the workaround ``` By following this approach, you can ensure that your code handles dynamic `replace` flags without encountering type issues. ## Common recipes ### Middleware that doesn't change the store type ```ts import { create, StateCreator, StoreMutatorIdentifier } from 'zustand' type Logger = < T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], >( f: StateCreator, name?: string, ) => StateCreator type LoggerImpl = ( f: StateCreator, name?: string, ) => StateCreator const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => { const loggedSet: typeof set = (...a) => { set(...(a as Parameters)) console.log(...(name ? [`${name}:`] : []), get()) } const setState = store.setState store.setState = (...a) => { setState(...(a as Parameters)) console.log(...(name ? [`${name}:`] : []), store.getState()) } return f(loggedSet, get, store) } export const logger = loggerImpl as unknown as Logger // --- const useBearStore = create()( logger( (set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), }), 'bear-store', ), ) ``` ### Middleware that changes the store type ```ts import { create, StateCreator, StoreMutatorIdentifier, Mutate, StoreApi, } from 'zustand' type Foo = < T, A, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], >( f: StateCreator, bar: A, ) => StateCreator declare module 'zustand' { interface StoreMutators { foo: Write, { foo: A }> } } type FooImpl = ( f: StateCreator, bar: A, ) => StateCreator const fooImpl: FooImpl = (f, bar) => (set, get, _store) => { type T = ReturnType type A = typeof bar const store = _store as Mutate, [['foo', A]]> store.foo = bar return f(set, get, _store) } export const foo = fooImpl as unknown as Foo type Write = Omit & U type Cast = T extends U ? T : U // --- const useBearStore = create(foo(() => ({ bears: 0 }), 'hello')) console.log(useBearStore.foo.toUpperCase()) ``` ### `create` without curried workaround The recommended way to use `create` is using the curried workaround like so: `create()(...)`. This is because it enables you to infer the store type. But if for some reason you do not want to use the workaround, you can pass the type parameters like the following. Note that in some cases, this acts as an assertion instead of annotation, so we don't recommend it. ```ts import { create } from "zustand" interface BearState { bears: number increase: (by: number) => void } const useBearStore = create< BearState, [ ['zustand/persist', BearState], ['zustand/devtools', never] ] >(devtools(persist((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), }), { name: 'bearStore' })) ``` ### Slices pattern ```ts import { create, StateCreator } from 'zustand' interface BearSlice { bears: number addBear: () => void eatFish: () => void } interface FishSlice { fishes: number addFish: () => void } interface SharedSlice { addBoth: () => void getBoth: () => number } const createBearSlice: StateCreator< BearSlice & FishSlice, [], [], BearSlice > = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), }) const createFishSlice: StateCreator< BearSlice & FishSlice, [], [], FishSlice > = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) const createSharedSlice: StateCreator< BearSlice & FishSlice, [], [], SharedSlice > = (set, get) => ({ addBoth: () => { // you can reuse previous methods get().addBear() get().addFish() // or do them from scratch // set((state) => ({ bears: state.bears + 1, fishes: state.fishes + 1 }) }, getBoth: () => get().bears + get().fishes, }) const useBoundStore = create()((...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), ...createSharedSlice(...a), })) ``` A detailed explanation on the slices pattern can be found [here](./slices-pattern.md). If you have some middlewares then replace `StateCreator` with `StateCreator`. For example, if you are using `devtools` then it will be `StateCreator`. See the ["Middlewares and their mutators reference"](#middlewares-and-their-mutators-reference) section for a list of all mutators. ### Bounded `useStore` hook for vanilla stores ```ts import { useStore } from 'zustand' import { createStore } from 'zustand/vanilla' interface BearState { bears: number increase: (by: number) => void } const bearStore = createStore()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) function useBearStore(): BearState function useBearStore(selector: (state: BearState) => T): T function useBearStore(selector?: (state: BearState) => T) { return useStore(bearStore, selector!) } ``` You can also make an abstract `createBoundedUseStore` function if you need to create bounded `useStore` hooks often and want to DRY things up... ```ts import { useStore, StoreApi } from 'zustand' import { createStore } from 'zustand/vanilla' interface BearState { bears: number increase: (by: number) => void } const bearStore = createStore()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) const createBoundedUseStore = ((store) => (selector) => useStore(store, selector)) as >( store: S, ) => { (): ExtractState (selector: (state: ExtractState) => T): T } type ExtractState = S extends { getState: () => infer X } ? X : never const useBearStore = createBoundedUseStore(bearStore) ``` ## Middlewares and their mutators reference - `devtools` — `["zustand/devtools", never]` - `persist` — `["zustand/persist", YourPersistedState]`
`YourPersistedState` is the type of state you are going to persist, ie the return type of `options.partialize`, if you're not passing `partialize` options the `YourPersistedState` becomes `Partial`. Also [sometimes](https://github.com/pmndrs/zustand/issues/980#issuecomment-1162289836) passing actual `PersistedState` won't work. In those cases, try passing `unknown`. - `immer` — `["zustand/immer", never]` - `subscribeWithSelector` — `["zustand/subscribeWithSelector", never]` - `redux` — `["zustand/redux", YourAction]` - `combine` — no mutator as `combine` does not mutate the store ================================================ FILE: docs/learn/guides/auto-generating-selectors.md ================================================ --- title: Auto Generating Selectors nav: 14 --- We recommend using selectors when using either the properties or actions from the store. You can access values from the store like so: ```typescript const bears = useBearStore((state) => state.bears) ``` However, writing these could be tedious. If that is the case for you, you can auto-generate your selectors. ## Create the following function: `createSelectors` ```typescript import { StoreApi, UseBoundStore } from 'zustand' type WithSelectors = S extends { getState: () => infer T } ? S & { use: { [K in keyof T]: () => T[K] } } : never const createSelectors = >>( _store: S, ) => { const store = _store as WithSelectors store.use = {} for (const k of Object.keys(store.getState())) { ;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s]) } return store } ``` If you have a store like this: ```typescript interface BearState { bears: number increase: (by: number) => void increment: () => void } const useBearStoreBase = create()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), increment: () => set((state) => ({ bears: state.bears + 1 })), })) ``` Apply that function to your store: ```typescript const useBearStore = createSelectors(useBearStoreBase) ``` Now the selectors are auto generated and you can access them directly: ```typescript // get the property const bears = useBearStore.use.bears() // get the action const increment = useBearStore.use.increment() ``` ## Vanilla Store If you are using a vanilla store, use the following `createSelectors` function: ```typescript import { StoreApi, useStore } from 'zustand' type WithSelectors = S extends { getState: () => infer T } ? S & { use: { [K in keyof T]: () => T[K] } } : never const createSelectors = >(_store: S) => { const store = _store as WithSelectors store.use = {} for (const k of Object.keys(store.getState())) { ;(store.use as any)[k] = () => useStore(_store, (s) => s[k as keyof typeof s]) } return store } ``` The usage is the same as a React store. If you have a store like this: ```typescript import { createStore } from 'zustand' interface BearState { bears: number increase: (by: number) => void increment: () => void } const store = createStore()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), increment: () => set((state) => ({ bears: state.bears + 1 })), })) ``` Apply that function to your store: ```typescript const useBearStore = createSelectors(store) ``` Now the selectors are auto generated and you can access them directly: ```typescript // get the property const bears = useBearStore.use.bears() // get the action const increment = useBearStore.use.increment() ``` ## Live Demo For a working example of this, see the [Code Sandbox](https://codesandbox.io/s/zustand-auto-generate-selectors-forked-rl8v5e?file=/src/selectors.ts). ## Third-party Libraries - [auto-zustand-selectors-hook](https://github.com/Albert-Gao/auto-zustand-selectors-hook) - [react-hooks-global-state](https://github.com/dai-shi/react-hooks-global-state) - [zustood](https://github.com/udecode/zustood) - [@davstack/store](https://github.com/DawidWraga/davstack) ================================================ FILE: docs/learn/guides/beginner-typescript.md ================================================ --- title: Beginner TypeScript Guide nav: 12 --- Zustand is a lightweight state manager, particularly used with React. Zustand avoids reducers, context, and boilerplate. Paired with TypeScript, you get a strongly typed store-state, actions, and selectors-with autocomplete and compile-time safety. In this basic guide we’ll cover: - Creating a typed store (state + actions) - Using the store in React components with type safety - Resetting the store safely with types - Extracting and reusing Store type (for props, tests, and utilities) - Composing multiple selectors and building derived state (with type inference and without extra re-renders) - Middlewares with TypeScript support (`combine`, `devtools`, `persist`) - Async actions with typed API responses - Working with `createWithEqualityFn` (enhanced `create` store function) - Structuring and coordinating multiple stores ### Creating a Store with State & Actions Here we describe state and actions using an Typescript interface. The `` generic forces the store to match this shape. This means if you forget a field or use the wrong type, TypeScript will complain. Unlike plain JS, this guarantees type-safe state management. The `create` function uses the curried form, which results in a store of type `UseBoundStore>`. ```ts // store.ts import { create } from 'zustand' // Define types for state & actions interface BearState { bears: number food: string feed: (food: string) => void } // Create store using the curried form of `create` export const useBearStore = create()((set) => ({ bears: 2, food: 'honey', feed: (food) => set(() => ({ food })), })) ``` ### Using the Store in Components Inside components, you can read state and call actions. Selectors `(s) => s.bears` subscribe to only what you need. This reduces re-renders and improves performance. JS can do this too, but with TS your IDE autocompletes state fields. ```tsx import { useBearStore } from './store' function BearCounter() { // Select only 'bears' to avoid unnecessary re-renders const bears = useBearStore((s) => s.bears) return

{bears} bears around

} ``` ### Resetting the Store Resetting is useful after logout or “clear session”. We use `typeof initialState` to avoid repeating property types. TypeScript updates automatically if `initialState` changes. This is safer and cleaner compared to JS. ```tsx import { create } from 'zustand' const initialState = { bears: 0, food: 'honey' } // Reuse state type dynamically type BearState = typeof initialState & { increase: (by: number) => void reset: () => void } const useBearStore = create()((set) => ({ ...initialState, increase: (by) => set((s) => ({ bears: s.bears + by })), reset: () => set(initialState), })) function ResetZoo() { const { bears, increase, reset } = useBearStore() return (
{bears}
) } ``` ### Extracting Types Zustand provides a built-in helper called `ExtractState`. This is useful for tests, utility functions, or component props. It returns the full type of your store’s state and actions without having to manually redefine them. Extracting the Store type: ```ts // store.ts import { create, type ExtractState } from 'zustand' export const useBearStore = create((set) => ({ bears: 3, food: 'honey', increase: (by: number) => set((s) => ({ bears: s.bears + by })), })) // Extract the type of the whole store state export type BearState = ExtractState ``` Using extracted type in tests: ```ts // test.cy.ts import { BearState } from './store.ts' test('should reset store', () => { const snapshot: BearState = useBearStore.getState() expect(snapshot.bears).toBeGreaterThanOrEqual(0) }) ``` and in utility function: ```ts // util.ts import { BearState } from './store.ts' function logBearState(state: BearState) { console.log(`We have ${state.bears} bears eating ${state.food}`) } logBearState(useBearStore.getState()) ``` ### Selectors #### Multiple Selectors Sometimes you need more than one property. Returning an object from the selector lets you access multiple fields at once. However, directly destructuring properties from that object can cause unnecessary re-renders. To avoid this, it’s recommended to wrap the selector with `useShallow`, which prevents re-renders when the selected values remain shallowly equal. This is more efficient than subscribing to the whole store. TypeScript ensures you can’t accidentally misspell `bears` or `food`. See the [API documentation](../../reference/hooks/use-shallow.md) for more details on `useShallow`. ```tsx import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' // Bear store with explicit types interface BearState { bears: number food: number } const useBearStore = create()(() => ({ bears: 2, food: 10, })) // In components, you can use both stores safely function MultipleSelectors() { const { bears, food } = useBearStore( useShallow((state) => ({ bears: state.bears, food: state.food })), ) return (
We have {food} units of food for {bears} bears
) } ``` #### Derived State with Selectors Not all values need to be stored directly - some can be computed from existing state. You can derive values using selectors. This avoids duplication and keeps the store minimal. TypeScript ensures `bears` is a number, so math is safe. ```tsx import { create } from 'zustand' interface BearState { bears: number foodPerBear: number } const useBearStore = create()(() => ({ bears: 3, foodPerBear: 2, })) function TotalFood() { // Derived value: required amount food for all bears const totalFood = useBearStore((s) => s.bears * s.foodPerBear) // don't need to have extra property `{ totalFood: 6 }` in your Store return
We need ${totalFood} jars of honey
} ``` ### Middlewares #### `combine` middleware This middleware separates initial state and actions, making the code cleaner. TS automatically infers types from the state and actions, no interface needed. This is different from JS, where type safety is missing. It’s a very popular style in TypeScript projects. See the [API documentation](../../reference/middlewares/combine.md) for more details. ```ts import { create } from 'zustand' import { combine } from 'zustand/middleware' interface BearState { bears: number increase: () => void } // State + actions are separated export const useBearStore = create()( combine({ bears: 0 }, (set) => ({ increase: () => set((s) => ({ bears: s.bears + 1 })), })), ) ``` #### `devtools` middleware This middleware connects Zustand to Redux DevTools. You can inspect changes, time-travel, and debug state. It’s extremely useful in development. TS ensures your actions and state remain type-checked even here. See the [API documentation](../../reference/middlewares/devtools.md) for more details. ```ts import { create } from 'zustand' import { devtools } from 'zustand/middleware' interface BearState { bears: number increase: () => void } export const useBearStore = create()( devtools((set) => ({ bears: 0, increase: () => set((s) => ({ bears: s.bears + 1 })), })), ) ``` #### `persist` middleware This middleware keeps your store in `localStorage` (or another storage). This means your bears survive a page refresh. Great for apps where persistence matters. In TS, the state type stays consistent, so no runtime surprises. See the [API documentation](../../reference/middlewares/persist.md) for more details. ```ts import { create } from 'zustand' import { persist } from 'zustand/middleware' interface BearState { bears: number increase: () => void } export const useBearStore = create()( persist( (set) => ({ bears: 0, increase: () => set((s) => ({ bears: s.bears + 1 })), }), { name: 'bear-storage' }, // localStorage key ), ) ``` ### Async Actions Actions can be async to fetch remote data. Here we fetch bears count and update state. TS enforces correct API response type (`BearData`). In JS you might misspell `count` - TS prevents that. ```ts import { create } from 'zustand' interface BearData { count: number } interface BearState { bears: number fetchBears: () => Promise } export const useBearStore = create()((set) => ({ bears: 0, fetchBears: async () => { const res = await fetch('/api/bears') const data: BearData = await res.json() set({ bears: data.count }) }, })) ``` ### `createWithEqualityFn` Variant of `create` with equality built-in. Useful if you always want custom equality checks. Not common, but shows Zustand’s flexibility. TS still keeps full type inference. See the [API documentation](../../reference/apis/create-with-equality-fn.md) for more details. ```ts import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' const useBearStore = createWithEqualityFn(() => ({ bears: 0, })) const bears = useBearStore((s) => s.bears, Object.is) // or const bears = useBearStore((s) => ({ bears: s.bears }), shallow) ``` ### Multiple Stores You can create more than one store for different domains. For example, `BearStore` manages bears and `FishStore` manages fish. This keeps state isolated and easier to maintain in larger apps. With TypeScript, each store has its own strict type - you can’t accidentally mix bears and fish. ```tsx import { create } from 'zustand' // Bear store with explicit types interface BearState { bears: number addBear: () => void } const useBearStore = create()((set) => ({ bears: 2, addBear: () => set((s) => ({ bears: s.bears + 1 })), })) // Fish store with explicit types interface FishState { fish: number addFish: () => void } const useFishStore = create()((set) => ({ fish: 5, addFish: () => set((s) => ({ fish: s.fish + 1 })), })) // In components, you can use both stores safely function Zoo() { const { bears, addBear } = useBearStore() const { fish, addFish } = useFishStore() return (
{bears} bears and {fish} fish
) } ``` ### Conclusion Zustand together with TypeScript provides a balance: you keep the simplicity of small, minimalistic stores, while gaining the safety of strong typing. You don’t need boilerplate or complex patterns - state and actions live side by side, fully typed, and ready to use. Start with a basic store to learn the pattern, then expand gradually: use `combine` for cleaner inference, `persist` for storage, and `devtools` for debugging. ================================================ FILE: docs/learn/guides/connect-to-state-with-url-hash.md ================================================ --- title: Connect to state with URL nav: 10 --- ## Connect State with URL Hash If you want to connect state of a store to URL hash, you can create your own hash storage. ```ts import { create } from 'zustand' import { persist, StateStorage, createJSONStorage } from 'zustand/middleware' const hashStorage: StateStorage = { getItem: (key): string => { const searchParams = new URLSearchParams(location.hash.slice(1)) const storedValue = searchParams.get(key) ?? '' return JSON.parse(storedValue) }, setItem: (key, newValue): void => { const searchParams = new URLSearchParams(location.hash.slice(1)) searchParams.set(key, JSON.stringify(newValue)) location.hash = searchParams.toString() }, removeItem: (key): void => { const searchParams = new URLSearchParams(location.hash.slice(1)) searchParams.delete(key) location.hash = searchParams.toString() }, } export const useBoundStore = create()( persist( (set, get) => ({ fishes: 0, addAFish: () => set({ fishes: get().fishes + 1 }), }), { name: 'food-storage', // unique name storage: createJSONStorage(() => hashStorage), }, ), ) ``` ## Persist and Connect State with URL Parameters (Example: URL Query Parameters) There are times when you want to conditionally connect the state to the URL. This example depicts usage of the URL query parameters while keeping it synced with another persistence implementation, like `localstorage`. If you want the URL params to always populate, the conditional check on `getUrlSearch()` can be removed. The implementation below will update the URL in place, without refresh, as the relevant states change. ```ts import { create } from 'zustand' import { persist, StateStorage, createJSONStorage } from 'zustand/middleware' const getUrlSearch = () => { return window.location.search.slice(1) } const persistentStorage: StateStorage = { getItem: (key): string => { // Check URL first if (getUrlSearch()) { const searchParams = new URLSearchParams(getUrlSearch()) const storedValue = searchParams.get(key) return JSON.parse(storedValue as string) } else { // Otherwise, we should load from localstorage or alternative storage return JSON.parse(localStorage.getItem(key) as string) } }, setItem: (key, newValue): void => { // Check if query params exist at all, can remove check if always want to set URL if (getUrlSearch()) { const searchParams = new URLSearchParams(getUrlSearch()) searchParams.set(key, JSON.stringify(newValue)) window.history.replaceState(null, '', `?${searchParams.toString()}`) } localStorage.setItem(key, JSON.stringify(newValue)) }, removeItem: (key): void => { const searchParams = new URLSearchParams(getUrlSearch()) searchParams.delete(key) window.location.search = searchParams.toString() }, } type LocalAndUrlStore = { typesOfFish: string[] addTypeOfFish: (fishType: string) => void numberOfBears: number setNumberOfBears: (newNumber: number) => void } const storageOptions = { name: 'fishAndBearsStore', storage: createJSONStorage(() => persistentStorage), } const useLocalAndUrlStore = create()( persist( (set) => ({ typesOfFish: [], addTypeOfFish: (fishType) => set((state) => ({ typesOfFish: [...state.typesOfFish, fishType] })), numberOfBears: 0, setNumberOfBears: (numberOfBears) => set(() => ({ numberOfBears })), }), storageOptions, ), ) export default useLocalAndUrlStore ``` When generating the URL from a component, you can call buildShareableUrl: ```ts const buildURLSuffix = (params, version = 0) => { const searchParams = new URLSearchParams() const zustandStoreParams = { state: { typesOfFish: params.typesOfFish, numberOfBears: params.numberOfBears, }, version: version, // version is here because that is included with how Zustand sets the state } // The URL param key should match the name of the store, as specified as in storageOptions above searchParams.set('fishAndBearsStore', JSON.stringify(zustandStoreParams)) return searchParams.toString() } export const buildShareableUrl = (params, version) => { return `${window.location.origin}?${buildURLSuffix(params, version)}` } ``` The generated URL would look like (here without any encoding, for readability): `https://localhost/search?fishAndBearsStore={"state":{"typesOfFish":["tilapia","salmon"],"numberOfBears":15},"version":0}}` ### Demo - Hash: https://stackblitz.com/edit/vitejs-vite-9vg24prg - Query: https://stackblitz.com/edit/vitejs-vite-hyc97ynf ================================================ FILE: docs/learn/guides/event-handler-in-pre-react-18.md ================================================ --- title: Calling actions outside a React event handler in pre React 18 nav: 11 --- 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. In order to fix this, the action needs to be wrapped in `unstable_batchedUpdates` like so: ```jsx import { unstable_batchedUpdates } from 'react-dom' // or 'react-native' const useFishStore = create((set) => ({ fishes: 0, increaseFishes: () => set((prev) => ({ fishes: prev.fishes + 1 })), })) const nonReactCallback = () => { unstable_batchedUpdates(() => { useFishStore.getState().increaseFishes() }) } ``` More details: https://github.com/pmndrs/zustand/issues/302 ================================================ FILE: docs/learn/guides/flux-inspired-practice.md ================================================ --- title: Flux inspired practice nav: 19 --- Although Zustand is an unopinionated library, we do recommend a few patterns. These are inspired by practices originally found in [Flux](https://github.com/facebookarchive/flux), and more recently [Redux](https://redux.js.org/understanding/thinking-in-redux/three-principles), so if you are coming from another library, you should feel right at home. However, Zustand does differ in some fundamental ways, so some terminology may not perfectly align to other libraries. ## Recommended patterns ### Single store Your applications global state should be located in a single Zustand store. If you have a large application, Zustand supports [splitting the store into slices](./slices-pattern.md). ### Use `set` / `setState` to update the store Always use `set` (or `setState`) to perform updates to your store. `set` (and `setState`) ensures the described update is correctly merged and listeners are appropriately notified. ### Colocate store actions In Zustand, state can be updated without the use of dispatched actions and reducers found in other Flux libraries. These store actions can be added directly to the store as shown below. Optionally, by using `setState` they can be [located external to the store](./practice-with-no-store-actions.md) ```js const useBoundStore = create((set) => ({ storeSliceA: ..., storeSliceB: ..., storeSliceC: ..., updateX: () => set(...), updateY: () => set(...), })) ``` ## Redux-like patterns If you can't live without Redux-like reducers, you can define a `dispatch` function on the root level of the store: ```typescript const types = { increase: 'INCREASE', decrease: 'DECREASE' } const reducer = (state, { type, by = 1 }) => { switch (type) { case types.increase: return { grumpiness: state.grumpiness + by } case types.decrease: return { grumpiness: state.grumpiness - by } } } const useGrumpyStore = create((set) => ({ grumpiness: 0, dispatch: (args) => set((state) => reducer(state, args)), })) const dispatch = useGrumpyStore((state) => state.dispatch) dispatch({ type: types.increase, by: 2 }) ``` 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. ```typescript import { redux } from 'zustand/middleware' const useReduxStore = create(redux(reducer, initialState)) ``` 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). ================================================ FILE: docs/learn/guides/how-to-reset-state.md ================================================ --- title: How to reset state nav: 20 --- The following pattern can be used to reset the state to its initial value. ```ts const useSomeStore = create()((set, get, store) => ({ // your code here reset: () => { set(store.getInitialState()) }, })) ``` Resetting multiple stores at once ```ts import type { StateCreator } from 'zustand' import { create: actualCreate } from 'zustand' const storeResetFns = new Set<() => void>() const resetAllStores = () => { storeResetFns.forEach((resetFn) => { resetFn() }) } export const create = (() => { return (stateCreator: StateCreator) => { const store = actualCreate(stateCreator) storeResetFns.add(() => { store.setState(store.getInitialState(), true) }) return store } }) as typeof actualCreate ``` ## Demo - Basic: https://stackblitz.com/edit/zustand-how-to-reset-state-basic - Advanced: https://stackblitz.com/edit/zustand-how-to-reset-state-advanced ================================================ FILE: docs/learn/guides/immutable-state-and-merging.md ================================================ --- title: Immutable state and merging nav: 7 --- Like with React's `useState`, we need to update state immutably. Here's a typical example: ```jsx import { create } from 'zustand' const useCountStore = create((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) ``` The `set` function is to update state in the store. Because the state is immutable, it should have been like this: ```js set((state) => ({ ...state, count: state.count + 1 })) ``` However, as this is a common pattern, `set` actually merges state, and we can skip the `...state` part: ```js set((state) => ({ count: state.count + 1 })) ``` ## Nested objects The `set` function merges state at only one level. If you have a nested object, you need to merge them explicitly. You will use the spread operator pattern like so: ```jsx import { create } from 'zustand' const useCountStore = create((set) => ({ nested: { count: 0 }, inc: () => set((state) => ({ nested: { ...state.nested, count: state.nested.count + 1 }, })), })) ``` For complex use cases, consider using some libraries that help with immutable updates. You can refer to [Updating nested state object values](./updating-state.md#deeply-nested-object). ## Replace flag To disable the merging behavior, you can specify a `replace` boolean value for `set` like so: ```js set((state) => newState, true) ``` ================================================ FILE: docs/learn/guides/initialize-state-with-props.md ================================================ --- title: Initialize state with props nav: 17 --- 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. ## Store creator with `createStore` ```ts import { createStore } from 'zustand' interface BearProps { bears: number } interface BearState extends BearProps { addBear: () => void } type BearStore = ReturnType const createBearStore = (initProps?: Partial) => { const DEFAULT_PROPS: BearProps = { bears: 0, } return createStore()((set) => ({ ...DEFAULT_PROPS, ...initProps, addBear: () => set((state) => ({ bears: ++state.bears })), })) } ``` ## Creating a context with `React.createContext` ```ts import { createContext } from 'react' export const BearContext = createContext(null) ``` ## Basic component usage ```tsx // Provider implementation import { useState } from 'react' function App() { const [store] = useState(() => createBearStore()) return ( ) } ``` ```tsx // Consumer component import { useContext } from 'react' import { useStore } from 'zustand' function BasicConsumer() { const store = useContext(BearContext) if (!store) throw new Error('Missing BearContext.Provider in the tree') const bears = useStore(store, (s) => s.bears) const addBear = useStore(store, (s) => s.addBear) return ( <>
{bears} Bears.
) } ``` ## Common patterns ### Wrapping the context provider ```tsx // Provider wrapper import { useState } from 'react' type BearProviderProps = React.PropsWithChildren function BearProvider({ children, ...props }: BearProviderProps) { const [store] = useState(() => createBearStore(props)) return {children} } ``` ### Extracting context logic into a custom hook ```tsx // Mimic the hook returned by `create` import { useContext } from 'react' import { useStore } from 'zustand' function useBearContext(selector: (state: BearState) => T): T { const store = useContext(BearContext) if (!store) throw new Error('Missing BearContext.Provider in the tree') return useStore(store, selector) } ``` ```tsx // Consumer usage of the custom hook function CommonConsumer() { const bears = useBearContext((s) => s.bears) const addBear = useBearContext((s) => s.addBear) return ( <>
{bears} Bears.
) } ``` ### Optionally use memoized selector for stable outputs ```tsx import { useShallow } from 'zustand/react/shallow' const meals = ['Salmon', 'Berries', 'Nuts'] function CommonConsumer() { const bearMealsOrder = useBearContext( useShallow((s) => Array.from({ length: s.bears }).map((_, index) => meals.at(index % meals.length), ), ), ) return ( <> Order:
    {bearMealsOrder.map((meal) => (
  • {meal}
  • ))}
) } ``` ### Optionally allow using a custom equality function ```tsx // Allow custom equality function by using useStoreWithEqualityFn instead of useStore import { useContext } from 'react' import { useStoreWithEqualityFn } from 'zustand/traditional' function useBearContext( selector: (state: BearState) => T, equalityFn?: (left: T, right: T) => boolean, ): T { const store = useContext(BearContext) if (!store) throw new Error('Missing BearContext.Provider in the tree') return useStoreWithEqualityFn(store, selector, equalityFn) } ``` ### Complete example ```tsx // Provider wrapper & custom hook consumer function App2() { return ( ) } ``` ================================================ FILE: docs/learn/guides/maps-and-sets-usage.md ================================================ --- title: Map and Set Usage nav: 8 --- # Map and Set in Zustand Map and Set are mutable data structures. To use them in Zustand, you must create new instances when updating. ## Map ### Reading a Map ```typescript const foo = useSomeStore((state) => state.foo) ``` ### Updating a Map Always create a new Map instance: ```ts // Update single entry set((state) => ({ foo: new Map(state.foo).set(key, value), })) // Delete entry set((state) => { const next = new Map(state.foo) next.delete(key) return { foo: next } }) // Update multiple entries set((state) => { const next = new Map(state.foo) next.set('key1', 'value1') next.set('key2', 'value2') return { foo: next } }) // Clear set({ foo: new Map() }) ``` ## Set ### Reading a Set ```ts const bar = useSomeStore((state) => state.bar) ``` ### Updating a Set Always create a new Set instance: ```ts // Add item set((state) => ({ bar: new Set(state.bar).add(item), })) // Delete item set((state) => { const next = new Set(state.bar) next.delete(item) return { bar: next } }) // Toggle item set((state) => { const next = new Set(state.bar) next.has(item) ? next.delete(item) : next.add(item) return { bar: next } }) // Clear set({ bar: new Set() }) ``` ## Why New Instances? Zustand detects changes by comparing references. Mutating a Map or Set doesn't change its reference: ```ts // ❌ Wrong - same reference, no re-render set((state) => { state.foo.set(key, value) return { foo: state.foo } }) // ✅ Correct - new reference, triggers re-render set((state) => ({ foo: new Map(state.foo).set(key, value), })) ``` ## Pitfall: Type Hints for Empty Collections Provide type hints when initializing empty Maps and Sets: ```ts { ids: new Set([] as string[]), users: new Map([] as [string, User][]) } ``` Without type hints, TypeScript infers `never[]` which prevents adding items later. ## Demos Basic: https://stackblitz.com/edit/vitejs-vite-5cu5ddvx ================================================ FILE: docs/learn/guides/nextjs.md ================================================ --- title: Setup with Next.js nav: 15 --- > [!NOTE] > We will be updating this guide soon based on our discussion in https://github.com/pmndrs/zustand/discussions/2740. [Next.js](https://nextjs.org) is a popular server-side rendering framework for React that presents some unique challenges for using Zustand properly. Keep in mind that Zustand store is a global variable (AKA module state) making it optional to use a `Context`. These challenges include: - **Per-request store:** A Next.js server can handle multiple requests simultaneously. This means that the store should be created per request and should not be shared across requests. - **SSR friendly:** Next.js applications are rendered twice, first on the server and again on the client. Having different outputs on both the client and the server will result in "hydration errors." The store will have to be initialized on the server and then re-initialized on the client with the same data in order to avoid that. Please read more about that in our [SSR and Hydration](./ssr-and-hydration.md) guide. - **SPA routing friendly:** Next.js supports a hybrid model for client side routing, which means that in order to reset a store, we need to initialize it at the component level using a `Context`. - **Server caching friendly:** Recent versions of Next.js (specifically applications using the App Router architecture) support aggressive server caching. Due to our store being a **module state**, it is completely compatible with this caching. We have these general recommendations for the appropriate use of Zustand: - **No global stores** - Because the store should not be shared across requests, it should not be defined as a global variable. Instead, the store should be created per request. - **React Server Components should not read from or write to the store** - RSCs cannot use hooks or context. They aren't meant to be stateful. Having an RSC read from or write values to a global store violates the architecture of Next.js. ### Creating a store per request Let's write our store factory function that will create a new store for each request. ```json // tsconfig.json { "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ``` > **Note:** do not forget to remove all comments from your `tsconfig.json` file. ### Initializing the store ```ts // src/stores/counter-store.ts import { createStore } from 'zustand/vanilla' export type CounterState = { count: number } export type CounterActions = { decrementCount: () => void incrementCount: () => void } export type CounterStore = CounterState & CounterActions export const defaultInitState: CounterState = { count: 0, } export const createCounterStore = ( initState: CounterState = defaultInitState, ) => { return createStore()((set) => ({ ...initState, decrementCount: () => set((state) => ({ count: state.count - 1 })), incrementCount: () => set((state) => ({ count: state.count + 1 })), })) } ``` ### Providing the store Let's use the `createCounterStore` in our component and share it using a context provider. ```tsx // src/providers/counter-store-provider.tsx 'use client' import { type ReactNode, createContext, useState, useContext } from 'react' import { useStore } from 'zustand' import { type CounterStore, createCounterStore } from '@/stores/counter-store' export type CounterStoreApi = ReturnType export const CounterStoreContext = createContext( undefined, ) export interface CounterStoreProviderProps { children: ReactNode } export const CounterStoreProvider = ({ children, }: CounterStoreProviderProps) => { const [store] = useState(() => createCounterStore()) return ( {children} ) } export const useCounterStore = ( selector: (store: CounterStore) => T, ): T => { const counterStoreContext = useContext(CounterStoreContext) if (!counterStoreContext) { throw new Error(`useCounterStore must be used within CounterStoreProvider`) } return useStore(counterStoreContext, selector) } ``` > **Note:** In this example, we ensure that this component is re-render-safe by checking the > value of the reference, so that the store is only created once. This component will only be > rendered once per request on the server, but might be re-rendered multiple times on the client if > there are stateful client components located above this component in the tree, or if this component > also contains other mutable state that causes a re-render. ### Using the store with different architectures There are two architectures for a Next.js application: the [Pages Router](https://nextjs.org/docs/pages/building-your-application/routing) and the [App Router](https://nextjs.org/docs/app/building-your-application/routing). The usage of Zustand on both architectures should be the same with slight differences related to each architecture. #### Pages Router ```tsx // src/components/pages/home-page.tsx import { useCounterStore } from '@/providers/counter-store-provider' export const HomePage = () => { const { count, incrementCount, decrementCount } = useCounterStore( (state) => state, ) return (
Count: {count}
) } ``` ```tsx // src/_app.tsx import type { AppProps } from 'next/app' import { CounterStoreProvider } from '@/providers/counter-store-provider' export default function App({ Component, pageProps }: AppProps) { return ( ) } ``` ```tsx // src/pages/index.tsx import { HomePage } from '@/components/pages/home-page' export default function Home() { return } ``` > **Note:** creating a store per route would require creating and sharing the store > at page (route) component level. Try not to use this if you do not need to create > a store per route. ```tsx // src/pages/index.tsx import { CounterStoreProvider } from '@/providers/counter-store-provider' import { HomePage } from '@/components/pages/home-page' export default function Home() { return ( ) } ``` #### App Router ```tsx // src/components/pages/home-page.tsx 'use client' import { useCounterStore } from '@/providers/counter-store-provider' export const HomePage = () => { const { count, incrementCount, decrementCount } = useCounterStore( (state) => state, ) return (
Count: {count}
) } ``` ```tsx // src/app/layout.tsx import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' import { CounterStoreProvider } from '@/providers/counter-store-provider' const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app', } export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( {children} ) } ``` ```tsx // src/app/page.tsx import { HomePage } from '@/components/pages/home-page' export default function Home() { return } ``` > **Note:** creating a store per route would require creating and sharing the store > at page (route) component level. Try not to use this if you do not need to create > a store per route. ```tsx // src/app/page.tsx import { CounterStoreProvider } from '@/providers/counter-store-provider' import { HomePage } from '@/components/pages/home-page' export default function Home() { return ( ) } ``` ================================================ FILE: docs/learn/guides/practice-with-no-store-actions.md ================================================ --- title: Practice with no store actions nav: 5 --- The recommended usage is to colocate actions and states within the store (let your actions be located together with your state). For example: ```js export const useBoundStore = create((set) => ({ count: 0, text: 'hello', inc: () => set((state) => ({ count: state.count + 1 })), setText: (text) => set({ text }), })) ``` This creates a self-contained store with data and actions together. --- An alternative approach is to define actions at module level, external to the store. ```js export const useBoundStore = create(() => ({ count: 0, text: 'hello', })) export const inc = () => useBoundStore.setState((state) => ({ count: state.count + 1 })) export const setText = (text) => useBoundStore.setState({ text }) ``` This has a few advantages: - It doesn't require a hook to call an action; - It facilitates code splitting. While this pattern doesn't offer any downsides, some may prefer colocating due to its encapsulated nature. ================================================ FILE: docs/learn/guides/prevent-rerenders-with-use-shallow.md ================================================ --- title: Prevent rerenders with useShallow nav: 9 --- When you need to subscribe to a computed state from a store, the recommended way is to use a selector. 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). In this case you might want to use `useShallow` to avoid a rerender if the computed value is always shallow equal the previous one. ## Example We have a store that associates to each bear a meal and we want to render their names. ```js import { create } from 'zustand' const useMeals = create(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', littleBear: 'A little, small, wee pot', })) export const BearNames = () => { const names = useMeals((state) => Object.keys(state)) return
{names.join(', ')}
} ``` Now papa bear wants a pizza instead: ```js useMeals.setState({ papaBear: 'a large pizza', }) ``` This change causes `BearNames` rerenders even though the actual output of `names` has not changed according to shallow equal. We can fix that using `useShallow`! ```js import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' const useMeals = create(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', littleBear: 'A little, small, wee pot', })) export const BearNames = () => { const names = useMeals(useShallow((state) => Object.keys(state))) return
{names.join(', ')}
} ``` Now they can all order other meals without causing unnecessary rerenders of our `BearNames` component. ================================================ FILE: docs/learn/guides/slices-pattern.md ================================================ --- title: Slices Pattern nav: 6 --- ## Slicing the store into smaller stores Your store can become bigger and bigger and tougher to maintain as you add more features. You can divide your main store into smaller individual stores to achieve modularity. This is simple to accomplish in Zustand! The first individual store: ```js export const createFishSlice = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) ``` Another individual store: ```js export const createBearSlice = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), }) ``` You can now combine both the stores into **one bounded store**: ```js import { create } from 'zustand' import { createBearSlice } from './bearSlice' import { createFishSlice } from './fishSlice' export const useBoundStore = create((...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), })) ``` ### Usage in a React component ```jsx import { useBoundStore } from './stores/useBoundStore' function App() { const bears = useBoundStore((state) => state.bears) const fishes = useBoundStore((state) => state.fishes) const addBear = useBoundStore((state) => state.addBear) return (

Number of bears: {bears}

Number of fishes: {fishes}

) } export default App ``` ### Updating multiple stores You can update multiple stores, at the same time, in a single function. ```js export const createBearFishSlice = (set, get) => ({ addBearAndFish: () => { get().addBear() get().addFish() }, }) ``` Combining all the stores together is the same as before. ```js import { create } from 'zustand' import { createBearSlice } from './bearSlice' import { createFishSlice } from './fishSlice' import { createBearFishSlice } from './createBearFishSlice' export const useBoundStore = create((...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), ...createBearFishSlice(...a), })) ``` ## Adding middlewares Adding middlewares to a combined store is the same as with other normal stores. Adding [`persist` middleware](../../reference/integrations/persisting-store-data.md) to our `useBoundStore`: ```js import { create } from 'zustand' import { createBearSlice } from './bearSlice' import { createFishSlice } from './fishSlice' import { persist } from 'zustand/middleware' export const useBoundStore = create( persist( (...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), }), { name: 'bound-store' }, ), ) ``` Please keep in mind you should only apply middlewares in the combined store. Applying them inside individual slices can lead to unexpected issues. ## Usage with TypeScript A detailed guide on how to use the slice pattern in Zustand with TypeScript can be found [here](./advanced-typescript.md#slices-pattern). ================================================ FILE: docs/learn/guides/ssr-and-hydration.md ================================================ --- title: SSR and Hydration nav: 16 --- ## Server-side Rendering (SSR) Server-side Rendering (SSR) is a technique that helps us render our components into HTML strings on the server, send them directly to the browser, and finally "hydrate" the static markup into a fully interactive app on the client. ### React Let's say we want to render a stateless app using React. In order to do that, we need to use `express`, `react` and `react-dom/server`. We don't need `react-dom/client` since it's a stateless app. Let's dive into that: - `express` helps us build a web app that we can run using Node, - `react` helps us build the UI components that we use in our app, - `react-dom/server` helps us render our components on a server. ```json // tsconfig.json { "compilerOptions": { "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": true, "target": "esnext" }, "include": ["**/*"] } ``` > **Note:** do not forget to remove all comments from your `tsconfig.json` file. ```tsx // app.tsx export const App = () => { return ( Static Server-side-rendered App
Hello World!
) } ``` ```tsx // server.tsx import express from 'express' import React from 'react' import ReactDOMServer from 'react-dom/server' import { App } from './app.tsx' const port = Number.parseInt(process.env.PORT || '3000', 10) const app = express() app.get('/', (_, res) => { const { pipe } = ReactDOMServer.renderToPipeableStream(, { onShellReady() { res.setHeader('content-type', 'text/html') pipe(res) }, }) }) app.listen(port, () => { console.log(`Server is listening at ${port}`) }) ``` ```sh tsc --build ``` ```sh node server.js ``` ## Hydration Hydration turns the initial HTML snapshot from the server into a fully interactive app that runs in the browser. The right way to "hydrate" a component is by using `hydrateRoot`. ### React Let's say we want to render a stateful app using React. In order to do that we need to use `express`, `react`, `react-dom/server` and `react-dom/client`. Let's dive into that: - `express` helps us build a web app that we can run using Node, - `react` helps us build the UI components that we use in our app, - `react-dom/server` helps us render our components on a server, - `react-dom/client` helps us hydrate our components on a client. > **Note:** Do not forget that even if we can render our components on a server, it is > important to "hydrate" them on a client to make them interactive. ```json // tsconfig.json { "compilerOptions": { "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": true, "target": "esnext" }, "include": ["**/*"] } ``` > **Note:** do not forget to remove all comments in your `tsconfig.json` file. ```tsx // app.tsx export const App = () => { return ( Static Server-side-rendered App
Hello World!
) } ``` ```tsx // main.tsx import ReactDOMClient from 'react-dom/client' import { App } from './app.tsx' ReactDOMClient.hydrateRoot(document, ) ``` ```tsx // server.tsx import express from 'express' import React from 'react' import ReactDOMServer from 'react-dom/server' import { App } from './app.tsx' const port = Number.parseInt(process.env.PORT || '3000', 10) const app = express() app.use('/', (_, res) => { const { pipe } = ReactDOMServer.renderToPipeableStream(, { bootstrapScripts: ['/main.js'], onShellReady() { res.setHeader('content-type', 'text/html') pipe(res) }, }) }) app.listen(port, () => { console.log(`Server is listening at ${port}`) }) ``` ```sh tsc --build ``` ```sh node server.js ``` > **Warning:** The React tree you pass to `hydrateRoot` needs to produce the same output as it did on the server. > The most common causes leading to hydration errors include: > > - Extra whitespace (like newlines) around the React-generated HTML inside the root node. > - Using checks like typeof window !== 'undefined' in your rendering logic. > - Using browser-only APIs like `window.matchMedia` in your rendering logic. > - Rendering different data on the server and the client. > > 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. You can read more about the caveats and pitfalls here: [hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot) ================================================ FILE: docs/learn/guides/testing.md ================================================ --- title: Testing description: Writing Tests nav: 18 --- ## Setting Up a Test Environment ### Test Runners Usually, your test runner needs to be configured to run JavaScript/TypeScript syntax. If you're going to be testing UI components, you will likely need to configure the test runner to use JSDOM to provide a mock DOM environment. See these resources for test runner configuration instructions: - **Jest** - [Jest: Getting Started](https://jestjs.io/docs/getting-started) - [Jest: Configuration - Test Environment](https://jestjs.io/docs/configuration#testenvironment-string) - **Vitest** - [Vitest: Getting Started](https://vitest.dev/guide) - [Vitest: Configuration - Test Environment](https://vitest.dev/config/#environment) ### UI and Network Testing Tools **We recommend using [React Testing Library (RTL)](https://testing-library.com/docs/react-testing-library/intro) to test out React components that connect to Zustand**. RTL is a simple and complete React DOM testing utility that encourages good testing practices. It uses ReactDOM's `render` function and `act` from `react-dom/tests-utils`. Furthermore, [Native Testing Library (RNTL)](https://testing-library.com/docs/react-native-testing-library/intro) is the alternative to RTL to test out React Native components. The [Testing Library](https://testing-library.com/) family of tools also includes adapters for many other popular frameworks. We also recommend using [Mock Service Worker (MSW)](https://mswjs.io/) to mock network requests, as this means your application logic does not need to be changed or mocked when writing tests. - **React Testing Library (DOM)** - [DOM Testing Library: Setup](https://testing-library.com/docs/dom-testing-library/setup) - [React Testing Library: Setup](https://testing-library.com/docs/react-testing-library/setup) - [Testing Library Jest-DOM Matchers](https://testing-library.com/docs/ecosystem-jest-dom) - **Native Testing Library (React Native)** - [Native Testing Library: Setup](https://testing-library.com/docs/react-native-testing-library/setup) - **User Event Testing Library (DOM)** - [User Event Testing Library: Setup](https://testing-library.com/docs/user-event/setup) - **TypeScript for Jest** - [TypeScript for Jest: Setup](https://kulshekhar.github.io/ts-jest/docs/getting-started/installation) - **TypeScript for Node** - [TypeScript for Node: Setup](https://typestrong.org/ts-node/docs/installation) - **Mock Service Worker** - [MSW: Installation](https://mswjs.io/docs/getting-started/install) - [MSW: Setting up mock requests](https://mswjs.io/docs/getting-started/mocks/rest-api) - [MSW: Mock server configuration for Node](https://mswjs.io/docs/getting-started/integrate/node) ## Setting Up Zustand for testing > **Note**: Since Jest and Vitest have slight differences, like Vitest using **ES modules** and Jest using > **CommonJS modules**, you need to keep that in mind if you are using Vitest instead of Jest. The mock provided below will enable the relevant test runner to reset the zustand stores after each test. ### Shared code just for testing purposes This shared code was added to avoid code duplication in our demo since we use the same counter store creator for both implementations, with and without `Context` API — `createStore` and `create`, respectively. ```ts // shared/counter-store-creator.ts import { type StateCreator } from 'zustand' export type CounterStore = { count: number inc: () => void } export const counterStoreCreator: StateCreator = (set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), }) ``` ### Jest In the next steps we are going to setup our Jest environment in order to mock Zustand. ```ts // __mocks__/zustand.ts import { act } from '@testing-library/react' import type * as ZustandExportedTypes from 'zustand' export * from 'zustand' const { create: actualCreate, createStore: actualCreateStore } = jest.requireActual('zustand') // a variable to hold reset functions for all stores declared in the app export const storeResetFns = new Set<() => void>() const createUncurried = ( stateCreator: ZustandExportedTypes.StateCreator, ) => { const store = actualCreate(stateCreator) const initialState = store.getInitialState() storeResetFns.add(() => { store.setState(initialState, true) }) return store } // when creating a store, we get its initial state, create a reset function and add it in the set export const create = (( stateCreator: ZustandExportedTypes.StateCreator, ) => { console.log('zustand create mock') // to support curried version of create return typeof stateCreator === 'function' ? createUncurried(stateCreator) : createUncurried }) as typeof ZustandExportedTypes.create const createStoreUncurried = ( stateCreator: ZustandExportedTypes.StateCreator, ) => { const store = actualCreateStore(stateCreator) const initialState = store.getInitialState() storeResetFns.add(() => { store.setState(initialState, true) }) return store } // when creating a store, we get its initial state, create a reset function and add it in the set export const createStore = (( stateCreator: ZustandExportedTypes.StateCreator, ) => { console.log('zustand createStore mock') // to support curried version of createStore return typeof stateCreator === 'function' ? createStoreUncurried(stateCreator) : createStoreUncurried }) as typeof ZustandExportedTypes.createStore // reset all stores after each test run afterEach(() => { act(() => { storeResetFns.forEach((resetFn) => { resetFn() }) }) }) ``` ```ts // setup-jest.ts import '@testing-library/jest-dom' ``` ```ts // jest.config.ts import type { JestConfigWithTsJest } from 'ts-jest' const config: JestConfigWithTsJest = { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['./setup-jest.ts'], } export default config ``` > **Note**: to use TypeScript we need to install two packages `ts-jest` and `ts-node`. ### Vitest In the next steps we are going to setup our Vitest environment in order to mock Zustand. > **Warning:** In Vitest you can change the [root](https://vitest.dev/config/#root). > Due to that, you need make sure that you are creating your `__mocks__` directory in the right place. > Let's say that you change the **root** to `./src`, that means you need to create a `__mocks__` > directory under `./src`. The end result would be `./src/__mocks__`, rather than `./__mocks__`. > Creating `__mocks__` directory in the wrong place can lead to issues when using Vitest. ```ts // __mocks__/zustand.ts import { act } from '@testing-library/react' import type * as ZustandExportedTypes from 'zustand' export * from 'zustand' const { create: actualCreate, createStore: actualCreateStore } = await vi.importActual('zustand') // a variable to hold reset functions for all stores declared in the app export const storeResetFns = new Set<() => void>() const createUncurried = ( stateCreator: ZustandExportedTypes.StateCreator, ) => { const store = actualCreate(stateCreator) const initialState = store.getInitialState() storeResetFns.add(() => { store.setState(initialState, true) }) return store } // when creating a store, we get its initial state, create a reset function and add it in the set export const create = (( stateCreator: ZustandExportedTypes.StateCreator, ) => { console.log('zustand create mock') // to support curried version of create return typeof stateCreator === 'function' ? createUncurried(stateCreator) : createUncurried }) as typeof ZustandExportedTypes.create const createStoreUncurried = ( stateCreator: ZustandExportedTypes.StateCreator, ) => { const store = actualCreateStore(stateCreator) const initialState = store.getInitialState() storeResetFns.add(() => { store.setState(initialState, true) }) return store } // when creating a store, we get its initial state, create a reset function and add it in the set export const createStore = (( stateCreator: ZustandExportedTypes.StateCreator, ) => { console.log('zustand createStore mock') // to support curried version of createStore return typeof stateCreator === 'function' ? createStoreUncurried(stateCreator) : createStoreUncurried }) as typeof ZustandExportedTypes.createStore // reset all stores after each test run afterEach(() => { act(() => { storeResetFns.forEach((resetFn) => { resetFn() }) }) }) ``` > **Note**: without [globals configuration](https://vitest.dev/config/#globals) enabled, we need > to add `import { afterEach, vi } from 'vitest'` at the top. ```ts // global.d.ts /// /// ``` > **Note**: without [globals configuration](https://vitest.dev/config/#globals) enabled, we do > need to remove `/// `. ```ts // setup-vitest.ts import '@testing-library/jest-dom/vitest' vi.mock('zustand') // to make it work like Jest (auto-mocking) ``` > **Note**: without [globals configuration](https://vitest.dev/config/#globals) enabled, we need > to add `import { vi } from 'vitest'` at the top. ```ts // vitest.config.ts import { defineConfig, mergeConfig } from 'vitest/config' import viteConfig from './vite.config' export default defineConfig((configEnv) => mergeConfig( viteConfig(configEnv), defineConfig({ test: { globals: true, environment: 'jsdom', setupFiles: ['./setup-vitest.ts'], }, }), ), ) ``` ### Testing Components In the next examples we are going to use `useCounterStore` > **Note**: all of these examples are written using TypeScript. ```ts // shared/counter-store-creator.ts import { type StateCreator } from 'zustand' export type CounterStore = { count: number inc: () => void } export const counterStoreCreator: StateCreator = (set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), }) ``` ```ts // stores/use-counter-store.ts import { create } from 'zustand' import { type CounterStore, counterStoreCreator, } from '../shared/counter-store-creator' export const useCounterStore = create()(counterStoreCreator) ``` ```tsx // contexts/use-counter-store-context.tsx import { type ReactNode, createContext, useContext, useState } from 'react' import { createStore } from 'zustand' import { useStoreWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' import { type CounterStore, counterStoreCreator, } from '../shared/counter-store-creator' export const createCounterStore = () => { return createStore(counterStoreCreator) } export type CounterStoreApi = ReturnType export const CounterStoreContext = createContext( undefined, ) export interface CounterStoreProviderProps { children: ReactNode } export const CounterStoreProvider = ({ children, }: CounterStoreProviderProps) => { const [store] = useState(() => createCounterStore()) return ( {children} ) } export type UseCounterStoreContextSelector = (store: CounterStore) => T export const useCounterStoreContext = ( selector: UseCounterStoreContextSelector, ): T => { const counterStoreContext = useContext(CounterStoreContext) if (counterStoreContext === undefined) { throw new Error( 'useCounterStoreContext must be used within CounterStoreProvider', ) } return useStoreWithEqualityFn(counterStoreContext, selector, shallow) } ``` ```tsx // components/counter/counter.tsx import { useCounterStore } from '../../stores/use-counter-store' export function Counter() { const { count, inc } = useCounterStore() return (

Counter Store

{count}

) } ``` ```ts // components/counter/index.ts export * from './counter' ``` ```tsx // components/counter/counter.test.tsx import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Counter } from './counter' describe('Counter', () => { test('should render with initial state of 1', async () => { renderCounter() expect(await screen.findByText(/^1$/)).toBeInTheDocument() expect( await screen.findByRole('button', { name: /one up/i }), ).toBeInTheDocument() }) test('should increase count by clicking a button', async () => { const user = userEvent.setup() renderCounter() expect(await screen.findByText(/^1$/)).toBeInTheDocument() await user.click(await screen.findByRole('button', { name: /one up/i })) expect(await screen.findByText(/^2$/)).toBeInTheDocument() }) }) const renderCounter = () => { return render() } ``` ```tsx // components/counter-with-context/counter-with-context.tsx import { CounterStoreProvider, useCounterStoreContext, } from '../../contexts/use-counter-store-context' const Counter = () => { const { count, inc } = useCounterStoreContext((state) => state) return (

Counter Store Context

{count}

) } export const CounterWithContext = () => { return ( ) } ``` ```tsx // components/counter-with-context/index.ts export * from './counter-with-context' ``` ```tsx // components/counter-with-context/counter-with-context.test.tsx import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { CounterWithContext } from './counter-with-context' describe('CounterWithContext', () => { test('should render with initial state of 1', async () => { renderCounterWithContext() expect(await screen.findByText(/^1$/)).toBeInTheDocument() expect( await screen.findByRole('button', { name: /one up/i }), ).toBeInTheDocument() }) test('should increase count by clicking a button', async () => { const user = userEvent.setup() renderCounterWithContext() expect(await screen.findByText(/^1$/)).toBeInTheDocument() await user.click(await screen.findByRole('button', { name: /one up/i })) expect(await screen.findByText(/^2$/)).toBeInTheDocument() }) }) const renderCounterWithContext = () => { return render() } ``` > **Note**: without [globals configuration](https://vitest.dev/config/#globals) enabled, we need > to add `import { describe, test, expect } from 'vitest'` at the top of each test file. ### Testing Stores In the next examples we are going to use `useCounterStore` > **Note**: all of these examples are written using TypeScript. ```ts // shared/counter-store-creator.ts import { type StateCreator } from 'zustand' export type CounterStore = { count: number inc: () => void } export const counterStoreCreator: StateCreator = (set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), }) ``` ```ts // stores/use-counter-store.ts import { create } from 'zustand' import { type CounterStore, counterStoreCreator, } from '../shared/counter-store-creator' export const useCounterStore = create()(counterStoreCreator) ``` ```tsx // contexts/use-counter-store-context.tsx import { type ReactNode, createContext, useContext, useState } from 'react' import { createStore } from 'zustand' import { useStoreWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' import { type CounterStore, counterStoreCreator, } from '../shared/counter-store-creator' export const createCounterStore = () => { return createStore(counterStoreCreator) } export type CounterStoreApi = ReturnType export const CounterStoreContext = createContext( undefined, ) export interface CounterStoreProviderProps { children: ReactNode } export const CounterStoreProvider = ({ children, }: CounterStoreProviderProps) => { const [store] = useState(() => createCounterStore()) return ( {children} ) } export type UseCounterStoreContextSelector = (store: CounterStore) => T export const useCounterStoreContext = ( selector: UseCounterStoreContextSelector, ): T => { const counterStoreContext = useContext(CounterStoreContext) if (counterStoreContext === undefined) { throw new Error( 'useCounterStoreContext must be used within CounterStoreProvider', ) } return useStoreWithEqualityFn(counterStoreContext, selector, shallow) } ``` ```tsx // components/counter/counter.tsx import { useCounterStore } from '../../stores/use-counter-store' export function Counter() { const { count, inc } = useCounterStore() return (

Counter Store

{count}

) } ``` ```ts // components/counter/index.ts export * from './counter' ``` ```tsx // components/counter/counter.test.tsx import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Counter, useCounterStore } from '../../../stores/use-counter-store.ts' describe('Counter', () => { test('should render with initial state of 1', async () => { renderCounter() expect(useCounterStore.getState().count).toBe(1) }) test('should increase count by clicking a button', async () => { const user = userEvent.setup() renderCounter() expect(useCounterStore.getState().count).toBe(1) await user.click(await screen.findByRole('button', { name: /one up/i })) expect(useCounterStore.getState().count).toBe(2) }) }) const renderCounter = () => { return render() } ``` ```tsx // components/counter-with-context/counter-with-context.tsx import { CounterStoreProvider, useCounterStoreContext, } from '../../contexts/use-counter-store-context' const Counter = () => { const { count, inc } = useCounterStoreContext((state) => state) return (

Counter Store Context

{count}

) } export const CounterWithContext = () => { return ( ) } ``` ```tsx // components/counter-with-context/index.ts export * from './counter-with-context' ``` ```tsx // components/counter-with-context/counter-with-context.test.tsx import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { CounterStoreContext } from '../../../contexts/use-counter-store-context' import { counterStoreCreator } from '../../../shared/counter-store-creator' describe('CounterWithContext', () => { test('should render with initial state of 1', async () => { const counterStore = counterStoreCreator() renderCounterWithContext(counterStore) expect(counterStore.getState().count).toBe(1) expect( await screen.findByRole('button', { name: /one up/i }), ).toBeInTheDocument() }) test('should increase count by clicking a button', async () => { const user = userEvent.setup() const counterStore = counterStoreCreator() renderCounterWithContext(counterStore) expect(counterStore.getState().count).toBe(1) await user.click(await screen.findByRole('button', { name: /one up/i })) expect(counterStore.getState().count).toBe(2) }) }) const renderCounterWithContext = (store) => { return render(, { wrapper: ({ children }) => ( {children} ), }) } ``` ## References - **React Testing Library**: [React Testing Library (RTL)](https://testing-library.com/docs/react-testing-library/intro) is a very lightweight solution for testing React components. It provides utility functions on top of `react-dom` and `react-dom/test-utils`, in a way that encourages better testing practices. Its primary guiding principle is: "The more your tests resemble the way your software is used, the more confidence they can give you." - **Native Testing Library**: [Native Testing Library (RNTL)](https://testing-library.com/docs/react-native-testing-library/intro) is a very lightweight solution for testing React Native components, similarly to RTL, but its functions are built on top of `react-test-renderer`. - **Testing Implementation Details**: Blog post by Kent C. Dodds on why he recommends to avoid [testing implementation details](https://kentcdodds.com/blog/testing-implementation-details). ## Demos - Jest: https://stackblitz.com/edit/jest-zustand - Vitest: https://stackblitz.com/edit/vitest-zustand ================================================ FILE: docs/learn/guides/tutorial-tic-tac-toe.md ================================================ --- title: 'Tutorial: Tic-Tac-Toe' description: Building a game nav: 3 --- # Tutorial: Tic-Tac-Toe ## Building a game You will build a small tic-tac-toe game during this tutorial. This tutorial does assume existing React knowledge. The techniques you'll learn in the tutorial are fundamental to building any React app, and fully understanding it will give you a deep understanding of React and Zustand. > [!NOTE] > This tutorial is crafted for those who learn best through hands-on experience and want to swiftly > create something tangible. It draws inspiration from React's tic-tac-toe tutorial. The tutorial is divided into several sections: - Setup for the tutorial will give you a starting point to follow the tutorial. - Overview will teach you the fundamentals of React: components, props, and state. - Completing the game will teach you the most common techniques in React development. - Adding time travel will give you a deeper insight into the unique strengths of React. ### What are you building? In this tutorial, you'll build an interactive tic-tac-toe game with React and Zustand. You can see what it will look like when you're finished here: ```jsx import { create } from 'zustand' import { combine } from 'zustand/middleware' const useGameStore = create( combine( { history: [Array(9).fill(null)], currentMove: 0, }, (set, get) => { return { setHistory: (nextHistory) => { set((state) => ({ history: typeof nextHistory === 'function' ? nextHistory(state.history) : nextHistory, })) }, setCurrentMove: (nextCurrentMove) => { set((state) => ({ currentMove: typeof nextCurrentMove === 'function' ? nextCurrentMove(state.currentMove) : nextCurrentMove, })) }, } }, ), ) function Square({ value, onSquareClick }) { return ( ) } function Board({ xIsNext, squares, onPlay }) { const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player onPlay(nextSquares) } return ( <>
{status}
{squares.map((_, i) => ( handleClick(i)} /> ))}
) } export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const currentMove = useGameStore((state) => state.currentMove) const setCurrentMove = useGameStore((state) => state.setCurrentMove) const xIsNext = currentMove % 2 === 0 const currentSquares = history[currentMove] function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares] setHistory(nextHistory) setCurrentMove(nextHistory.length - 1) } function jumpTo(nextMove) { setCurrentMove(nextMove) } return (
    {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return (
  1. ) })}
) } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i] if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a] } } return null } function calculateTurns(squares) { return squares.filter((square) => !square).length } function calculateStatus(winner, turns, player) { if (!winner && !turns) return 'Draw' if (winner) return `Winner ${winner}` return `Next player: ${player}` } ``` ### Building the board Let's start by creating the `Square` component, which will be a building block for our `Board` component. This component will represent each square in our game. The `Square` component should take `value` and `onSquareClick` as props. It should return a ` ) } ``` Let's move on to creating the Board component, which will consist of 9 squares arranged in a grid. This component will serve as the main playing area for our game. The `Board` component should return a `
` element styled as a grid. The grid layout is achieved using CSS Grid, with three columns and three rows, each taking up an equal fraction of the available space. The overall size of the grid is determined by the width and height properties, ensuring that it is square-shaped and appropriately sized. Inside the grid, we place nine Square components, each with a value prop representing its position. These Square components will eventually hold the game symbols (`'X'` or `'O'`) and handle user interactions. Here's the code for the `Board` component: ```jsx export default function Board() { return (
) } ``` This Board component sets up the basic structure for our game board by arranging nine squares in a 3x3 grid. It positions the squares neatly, providing a foundation for adding more features and handling player interactions in the future. ### Lifting state up Each `Square` component could maintain a part of the game's state. To check for a winner in a tic-tac-toe game, the `Board` component would need to somehow know the state of each of the 9 `Square` components. How would you approach that? At first, you might guess that the `Board` component needs to ask each `Square` component for that `Square`'s component state. Although this approach is technically possible in React, we discourage it because the code becomes difficult to understand, susceptible to bugs, and hard to refactor. Instead, the best approach is to store the game's state in the parent `Board` component instead of in each `Square` component. The `Board` component can tell each `Square` component what to display by passing a prop, like you did when you passed a number to each `Square` component. > [!IMPORTANT] > To collect data from multiple children, or to have two or more child components > communicate with each other, declare the shared state in their parent component instead. The > parent component can pass that state back down to the children via props. This keeps the child > components in sync with each other and with their parent. Let's take this opportunity to try it out. Edit the `Board` component so that it declares a state variable named squares that defaults to an array of 9 nulls corresponding to the 9 squares: ```jsx import { create } from 'zustand' import { combine } from 'zustand/middleware' const useGameStore = create( combine({ squares: Array(9).fill(null) }, (set) => { return { setSquares: (nextSquares) => { set((state) => ({ squares: typeof nextSquares === 'function' ? nextSquares(state.squares) : nextSquares, })) }, } }), ) export default function Board() { const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) return (
{squares.map((square, squareIndex) => ( ))}
) } ``` `Array(9).fill(null)` creates an array with nine elements and sets each of them to `null`. The `useGameStore` declares a `squares` state that's initially set to that array. Each entry in the array corresponds to the value of a square. When you fill the board in later, the squares array will look like this: ```js const squares = ['O', null, 'X', 'X', 'X', 'O', 'O', null, null] ``` Each Square will now receive a `value` prop that will either be `'X'`, `'O'`, or `null` for empty squares. Next, you need to change what happens when a `Square` component is clicked. The `Board` component now maintains which squares are filled. You'll need to create a way for the `Square` component to update the `Board`'s component state. Since state is private to a component that defines it, you cannot update the `Board`'s component state directly from `Square` component. Instead, you'll pass down a function from the Board component to the `Square` component, and you'll have `Square` component call that function when a square is clicked. You'll start with the function that the `Square` component will call when it is clicked. You'll call that function `onSquareClick`: Now you'll connect the `onSquareClick` prop to a function in the `Board` component that you'll name `handleClick`. To connect `onSquareClick` to `handleClick` you'll pass an inline function to the `onSquareClick` prop of the first Square component: ```jsx handleClick(i)} /> ``` Lastly, you will define the `handleClick` function inside the `Board` component to update the squares array holding your board's state. The `handleClick` function should take the index of the square to update and create a copy of the `squares` array (`nextSquares`). Then, `handleClick` updates the `nextSquares` array by adding `X` to the square at the specified index (`i`) if is not already filled. ```jsx {5-10,27} export default function Board() { const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) function handleClick(i) { if (squares[i]) return const nextSquares = squares.slice() nextSquares[i] = 'X' setSquares(nextSquares) } return (
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } ``` > [!IMPORTANT] > Note how in `handleClick` function, you call `.slice()` to create a copy of the squares array > instead of modifying the existing array. ### Taking turns It's now time to fix a major defect in this tic-tac-toe game: the `'O'`s cannot be used on the board. You'll set the first move to be `'X'` by default. Let's keep track of this by adding another piece of state to the `useGameStore` hook: ```jsx {2,12-18} const useGameStore = create( combine({ squares: Array(9).fill(null), xIsNext: true }, (set) => { return { setSquares: (nextSquares) => { set((state) => ({ squares: typeof nextSquares === 'function' ? nextSquares(state.squares) : nextSquares, })) }, setXIsNext: (nextXIsNext) => { set((state) => ({ xIsNext: typeof nextXIsNext === 'function' ? nextXIsNext(state.xIsNext) : nextXIsNext, })) }, } }), ) ``` Each time a player moves, `xIsNext` (a boolean) will be flipped to determine which player goes next and the game's state will be saved. You'll update the Board's `handleClick` function to flip the value of `xIsNext`: ```jsx {2-3,6,11} export default function Board() { const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) const player = xIsNext ? 'X' : 'O' function handleClick(i) { if (squares[i]) return const nextSquares = squares.slice() nextSquares[i] = player setSquares(nextSquares) setXIsNext(!xIsNext) } return (
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } ``` ### Declaring a winner or draw Now that the players can take turns, you'll want to show when the game is won or drawn and there are no more turns to make. To do this you'll add three helper functions. The first helper function called `calculateWinner` that takes an array of 9 squares, checks for a winner and returns `'X'`, `'O'`, or `null` as appropriate. The second helper function called `calculateTurns` that takes the same array, checks for remaining turns by filtering out only `null` items, and returns the count of them. The last helper called `calculateStatus` that takes the remaining turns, the winner, and the current player (`'X' or 'O'`): ```js function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i] if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a] } } return null } function calculateTurns(squares) { return squares.filter((square) => !square).length } function calculateStatus(winner, turns, player) { if (!winner && !turns) return 'Draw' if (winner) return `Winner ${winner}` return `Next player: ${player}` } ``` You will use the result of `calculateWinner(squares)` in the Board component's `handleClick` function to check if a player has won. You can perform this check at the same time you check if a user has clicked a square that already has a `'X'` or and `'O'`. We'd like to return early in both cases: ```js {2} function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player setSquares(nextSquares) setXIsNext(!xIsNext) } ``` To let the players know when the game is over, you can display text such as `'Winner: X'` or `'Winner: O'`. To do that you'll add a `status` section to the `Board` component. The status will display the winner or draw if the game is over and if the game is ongoing you'll display which player's turn is next: ```jsx {6-7,9,21} export default function Board() { const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player setSquares(nextSquares) setXIsNext(!xIsNext) } return ( <>
{status}
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } ``` Congratulations! You now have a working tic-tac-toe game. And you've just learned the basics of React and Zustand too. So you are the real winner here. Here is what the code should look like: ```jsx import { create } from 'zustand' import { combine } from 'zustand/middleware' const useGameStore = create( combine({ squares: Array(9).fill(null), xIsNext: true }, (set) => { return { setSquares: (nextSquares) => { set((state) => ({ squares: typeof nextSquares === 'function' ? nextSquares(state.squares) : nextSquares, })) }, setXIsNext: (nextXIsNext) => { set((state) => ({ xIsNext: typeof nextXIsNext === 'function' ? nextXIsNext(state.xIsNext) : nextXIsNext, })) }, } }), ) function Square({ value, onSquareClick }) { return ( ) } export default function Board() { const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player setSquares(nextSquares) setXIsNext(!xIsNext) } return ( <>
{status}
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i] if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a] } } return null } function calculateTurns(squares) { return squares.filter((square) => !square).length } function calculateStatus(winner, turns, player) { if (!winner && !turns) return 'Draw' if (winner) return `Winner ${winner}` return `Next player: ${player}` } ``` ### Adding time travel As a final exercise, let's make it possible to “go back in time” and revisit previous moves in the game. If you had directly modified the squares array, implementing this time-travel feature would be very difficult. However, since you used `slice()` to create a new copy of the squares array after every move, treating it as immutable, you can store every past version of the squares array and navigate between them. You'll keep track of these past squares arrays in a new state variable called `history`. This `history` array will store all board states, from the first move to the latest one, and will look something like this: ```js const history = [ // First move [null, null, null, null, null, null, null, null, null], // Second move ['X', null, null, null, null, null, null, null, null], // Third move ['X', 'O', null, null, null, null, null, null, null], // and so on... ] ``` This approach allows you to easily navigate between different game states and implement the time-travel feature. ### Lifting state up, again Next, you will create a new top-level component called `Game` to display a list of past moves. This is where you will store the `history` state that contains the entire game history. By placing the `history` state in the `Game` component, you can remove the `squares` state from the `Board` component. You will now lift the state up from the `Board` component to the top-level `Game` component. This change allows the `Game` component to have full control over the `Board`'s component data and instruct the `Board` component to render previous turns from the `history`. First, add a `Game` component with `export default` and remove it from `Board` component. Here is what the code should look like: ```jsx {1,44-61} function Board() { const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player setSquares(nextSquares) setXIsNext(!xIsNext) } return ( <>
{status}
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } export default function Game() { return (
    {/* TODO */}
) } ``` Add some state to the `useGameStore` hook to track the history of moves: ```js {2,4-11} const useGameStore = create( combine({ history: [Array(9).fill(null)], xIsNext: true }, (set) => { return { setHistory: (nextHistory) => { set((state) => ({ history: typeof nextHistory === 'function' ? nextHistory(state.history) : nextHistory, })) }, setXIsNext: (nextXIsNext) => { set((state) => ({ xIsNext: typeof nextXIsNext === 'function' ? nextXIsNext(state.xIsNext) : nextXIsNext, })) }, } }), ) ``` Notice how `[Array(9).fill(null)]` creates an array with a single item, which is itself an array of 9 null values. To render the squares for the current move, you'll need to read the most recent squares array from the `history` state. You don't need an extra state for this because you already have enough information to calculate it during rendering: ```jsx {2-6} export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const currentSquares = history[history.length - 1] return (
    {/*TODO*/}
) } ``` Next, create a `handlePlay` function inside the `Game` component that will be called by the `Board` component to update the game. Pass `xIsNext`, `currentSquares` and `handlePlay` as props to the `Board` component: ```jsx {8-10,21} export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const currentSquares = history[history.length - 1] function handlePlay(nextSquares) { // TODO } return (
    {/*TODO*/}
) } ``` Let's make the `Board` component fully controlled by the props it receives. To do this, we'll modify the `Board` component to accept three props: `xIsNext`, `squares`, and a new `onPlay` function that the `Board` component can call with the updated squares array when a player makes a move. ```jsx {1} function Board({ xIsNext, squares, onPlay }) { const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player onPlay(nextSquares) } return ( <>
{status}
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } ``` The `Board` component is now fully controlled by the props passed to it by the `Game` component. To get the game working again, you need to implement the `handlePlay` function in the `Game` component. What should `handlePlay` do when called? Previously, the `Board` component called `setSquares` with an updated array; now it passes the updated squares array to `onPlay`. The `handlePlay` function needs to update the `Game` component's state to trigger a re-render. Instead of using `setSquares`, you'll update the `history` state variable by appending the updated squares array as a new `history` entry. You also need to toggle `xIsNext`, just as the `Board` component used to do. ```js {2-3} function handlePlay(nextSquares) { setHistory(history.concat([nextSquares])) setXIsNext(!xIsNext) } ``` At this point, you've moved the state to live in the `Game` component, and the UI should be fully working, just as it was before the refactor. Here is what the code should look like at this point: ```jsx import { create } from 'zustand' import { combine } from 'zustand/middleware' const useGameStore = create( combine({ history: [Array(9).fill(null)], xIsNext: true }, (set) => { return { setHistory: (nextHistory) => { set((state) => ({ history: typeof nextHistory === 'function' ? nextHistory(state.history) : nextHistory, })) }, setXIsNext: (nextXIsNext) => { set((state) => ({ xIsNext: typeof nextXIsNext === 'function' ? nextXIsNext(state.xIsNext) : nextXIsNext, })) }, } }), ) function Square({ value, onSquareClick }) { return ( ) } function Board({ xIsNext, squares, onPlay }) { const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player onPlay(nextSquares) } return ( <>
{status}
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const currentSquares = history[history.length - 1] function handlePlay(nextSquares) { setHistory(history.concat([nextSquares])) setXIsNext(!xIsNext) } return (
    {/*TODO*/}
) } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i] if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a] } } return null } function calculateTurns(squares) { return squares.filter((square) => !square).length } function calculateStatus(winner, turns, player) { if (!winner && !turns) return 'Draw' if (winner) return `Winner ${winner}` return `Next player: ${player}` } ``` ### Showing the past moves Since you are recording the tic-tac-toe game's history, you can now display a list of past moves to the player. You already have an array of `history` moves in store, so now you need to transform it to an array of React elements. In JavaScript, to transform one array into another, you can use the Array `.map()` method: You'll use `map` to transform your `history` of moves into React elements representing buttons on the screen, and display a list of buttons to **jump** to past moves. Let's `map` over the `history` in the `Game` component: ```jsx {29-44} export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const currentSquares = history[history.length - 1] function handlePlay(nextSquares) { setHistory(history.concat([nextSquares])) setXIsNext(!xIsNext) } function jumpTo(nextMove) { // TODO } return (
    {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return (
  1. ) })}
) } ``` Before you can implement the `jumpTo` function, you need the `Game` component to keep track of which step the user is currently viewing. To do this, define a new state variable called `currentMove`, which will start at `0`: ```js {3,14-21} const useGameStore = create( combine( { history: [Array(9).fill(null)], currentMove: 0, xIsNext: true }, (set) => { return { setHistory: (nextHistory) => { set((state) => ({ history: typeof nextHistory === 'function' ? nextHistory(state.history) : nextHistory, })) }, setCurrentMove: (nextCurrentMove) => { set((state) => ({ currentMove: typeof nextCurrentMove === 'function' ? nextCurrentMove(state.currentMove) : nextCurrentMove, })) }, setXIsNext: (nextXIsNext) => { set((state) => ({ xIsNext: typeof nextXIsNext === 'function' ? nextXIsNext(state.xIsNext) : nextXIsNext, })) }, } }, ), ) ``` Next, update the `jumpTo` function inside `Game` component to update that `currentMove`. You’ll also set `xIsNext` to `true` if the number that you’re changing `currentMove` to is even. ```js {2-3} function jumpTo(nextMove) { setCurrentMove(nextMove) setXIsNext(currentMove % 2 === 0) } ``` You will now make two changes to the `handlePlay` function in the `Game` component, which is called when you click on a square. - If you "go back in time" and then make a new move from that point, you only want to keep the history up to that point. Instead of adding `nextSquares` after all items in the history (using the Array `.concat()` method), you'll add it after all items in `history.slice(0, currentMove + 1)` to keep only that portion of the old history. - Each time a move is made, you need to update `currentMove` to point to the latest history entry. ```js {2-4} function handlePlay(nextSquares) { const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) setHistory(nextHistory) setCurrentMove(nextHistory.length - 1) setXIsNext(!xIsNext) } ``` Finally, you will modify the `Game` component to render the currently selected move, instead of always rendering the final move: ```jsx {2-8} export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const currentMove = useGameStore((state) => state.currentMove) const setCurrentMove = useGameStore((state) => state.setCurrentMove) const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const currentSquares = history[currentMove] function handlePlay(nextSquares) { const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) setHistory(nextHistory) setCurrentMove(nextHistory.length - 1) setXIsNext(!xIsNext) } function jumpTo(nextMove) { setCurrentMove(nextMove) setXIsNext(nextMove % 2 === 0) } return (
    {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return (
  1. ) })}
) } ``` ### Final cleanup If you look closely at the code, you'll see that `xIsNext` is `true` when `currentMove` is even and `false` when `currentMove` is odd. This means that if you know the value of `currentMove`, you can always determine what `xIsNext` should be. There's no need to store `xIsNext` separately in the state. It’s better to avoid redundant state because it can reduce bugs and make your code easier to understand. Instead, you can calculate `xIsNext` based on `currentMove`: ```jsx {2-5,13,17} export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const currentMove = useGameStore((state) => state.currentMove) const setCurrentMove = useGameStore((state) => state.setCurrentMove) const xIsNext = currentMove % 2 === 0 const currentSquares = history[currentMove] function handlePlay(nextSquares) { const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) setHistory(nextHistory) setCurrentMove(nextHistory.length - 1) } function jumpTo(nextMove) { setCurrentMove(nextMove) } return (
    {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return (
  1. ) })}
) } ``` You no longer need the `xIsNext` state declaration or the calls to `setXIsNext`. Now, there’s no chance for `xIsNext` to get out of sync with `currentMove`, even if you make a mistake while coding the components. ### Wrapping up Congratulations! You’ve created a tic-tac-toe game that: - Lets you play tic-tac-toe, - Indicates when a player has won the game or when is drawn, - Stores a game’s history as a game progresses, - Allows players to review a game’s history and see previous versions of a game’s board. Nice work! We hope you now feel like you have a decent grasp of how React and Zustand works. ================================================ FILE: docs/learn/guides/updating-state.md ================================================ --- title: Updating state nav: 4 --- ## Flat updates Updating state with Zustand is simple! Call the provided `set` function with the new state, and it will be shallowly merged with the existing state in the store. **Note** See next section for nested state. ```tsx import { create } from 'zustand' type State = { firstName: string lastName: string } type Action = { updateFirstName: (firstName: State['firstName']) => void updateLastName: (lastName: State['lastName']) => void } // Create your store, which includes both state and (optionally) actions const usePersonStore = create((set) => ({ firstName: '', lastName: '', updateFirstName: (firstName) => set(() => ({ firstName: firstName })), updateLastName: (lastName) => set(() => ({ lastName: lastName })), })) // In consuming app function App() { // "select" the needed state and actions, in this case, the firstName value // and the action updateFirstName const firstName = usePersonStore((state) => state.firstName) const updateFirstName = usePersonStore((state) => state.updateFirstName) return (

Hello, {firstName}!

) } ``` ## Deeply nested object If you have a deep state object like this: ```ts type State = { deep: { nested: { obj: { count: number } } } } ``` Updating nested state requires some effort to ensure the process is completed immutably. ### Normal approach Similar to React or Redux, the normal approach is to copy each level of the state object. This is done with the spread operator `...`, and by manually merging that in with the new state values. Like so: ```ts normalInc: () => set((state) => ({ deep: { ...state.deep, nested: { ...state.deep.nested, obj: { ...state.deep.nested.obj, count: state.deep.nested.obj.count + 1 } } } })), ``` This is very long! Let's explore some alternatives that will make your life easier. ### With Immer Many people use [Immer](https://github.com/immerjs/immer) to update nested values. Immer can be used anytime you need to update nested state such as in React, Redux and of course, Zustand! You can use Immer to shorten your state updates for deeply nested object. Let's take a look at an example: ```ts immerInc: () => set(produce((state: State) => { ++state.deep.nested.obj.count })), ``` What a reduction! Please take note of the [gotchas listed here](../../reference/integrations/immer-middleware.md). ### With optics-ts There is another option with [optics-ts](https://github.com/akheron/optics-ts/): ```ts opticsInc: () => set(O.modify(O.optic().path("deep.nested.obj.count"))((c) => c + 1)), ``` Unlike Immer, optics-ts doesn't use proxies or mutation syntax. ### With Ramda You can also use [Ramda](https://ramdajs.com/): ```ts ramdaInc: () => set(R.modifyPath(["deep", "nested", "obj", "count"], (c) => c + 1)), ``` Both ramda and optics-ts also work with types. ### Demo https://stackblitz.com/edit/vitejs-vite-j6bjdygu ================================================ FILE: docs/learn/index.md ================================================ --- title: Learn description: A guided path to understand Zustand fundamentals, common patterns, and when to reach for specific tools. --- ## Start here If you are new to Zustand, begin here for installation, a high-level overview, and a hands-on tutorial. - [Introduction](./getting-started/introduction.md) — Install Zustand and create your first store. - [Comparison with other tools](./getting-started/comparison.md) — See how Zustand compares to Redux, Jotai, Recoil, and others. - [Tutorial: Tic Tac Toe](./guides/tutorial-tic-tac-toe.md) — Build a complete game to learn Zustand concepts step by step. ## Core concepts The fundamentals of reading and updating state in a Zustand store. - [Updating state](./guides/updating-state.md) — How to update primitive values, objects, and nested state. - [Practice with no store actions](./guides/practice-with-no-store-actions.md) — Define state updates outside the store for simpler patterns. - [Slices pattern](./guides/slices-pattern.md) — Split a large store into smaller, composable slices. - [Immutable state and merging](./guides/immutable-state-and-merging.md) — Understand how Zustand merges state and when to spread manually. - [Maps and sets usage](./guides/maps-and-sets-usage.md) — Work with `Map` and `Set` inside Zustand state correctly. ## Performance and rendering Techniques for keeping re-renders minimal and components fast. - [Prevent rerenders with useShallow](./guides/prevent-rerenders-with-use-shallow.md) — Use shallow comparison to avoid unnecessary re-renders when selecting objects. - [Connect to state with URL hash](./guides/connect-to-state-with-url-hash.md) — Sync store state with the URL hash for shareable UI state. - [Event handler in pre React 18](./guides/event-handler-in-pre-react-18.md) — Handle the batching edge case in React 17 and earlier. ## TypeScript path Guides for typing stores, actions, and selectors with TypeScript. - [Beginner TypeScript](./guides/beginner-typescript.md) — Type a basic store with state and actions. - [Advanced TypeScript](./guides/advanced-typescript.md) — Type slices, middleware stacks, and complex patterns. - [Auto-generating selectors](./guides/auto-generating-selectors.md) — Generate typed selectors automatically from a store definition. ## Frameworks and platforms Using Zustand in server-rendered and framework-specific environments. - [Next.js](./guides/nextjs.md) — Set up Zustand in a Next.js app with proper SSR handling. - [SSR and hydration](./guides/ssr-and-hydration.md) — Avoid hydration mismatches when rendering on the server. - [Initialize state with props](./guides/initialize-state-with-props.md) — Seed a store's initial state from React component props. ## Testing and quality Best practices for writing reliable, maintainable code with Zustand. - [Testing stores and components](./guides/testing.md) — Test store logic and React components that consume a store. - [Flux-inspired practice](./guides/flux-inspired-practice.md) — Apply Flux conventions to keep state changes predictable. - [How to reset state](./guides/how-to-reset-state.md) — Reset a store back to its initial state on demand. ================================================ FILE: docs/reference/apis/create-store.md ================================================ --- title: createStore description: How to create vanilla stores nav: 22 --- `createStore` lets you create a vanilla store that exposes API utilities. ```js const someStore = createStore(stateCreatorFn) ``` - [Types](#types) - [Signature](#signature) - [Reference](#reference) - [Usage](#usage) - [Updating state based on previous state](#updating-state-based-on-previous-state) - [Updating Primitives in State](#updating-primitives-in-state) - [Updating Objects in State](#updating-objects-in-state) - [Updating Arrays in State](#updating-arrays-in-state) - [Subscribing to state updates](#subscribing-to-state-updates) - [Troubleshooting](#troubleshooting) - [I’ve updated the state, but the screen doesn’t update](#i’ve-updated-the-state,-but-the-screen-doesn’t-update) ## Types ### Signature ```ts createStore()(stateCreatorFn: StateCreator): StoreApi ``` ## Reference ### `createStore(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. #### Returns `createStore` returns a vanilla store that exposes API utilities, `setState`, `getState`, `getInitialState` and `subscribe`. ## Usage ### Updating state based on previous state This example shows how you can support **updater functions** within **actions**. ```tsx import { createStore } from 'zustand/vanilla' type AgeStoreState = { age: number } type AgeStoreActions = { setAge: ( nextAge: | AgeStoreState['age'] | ((currentAge: AgeStoreState['age']) => AgeStoreState['age']), ) => void } type AgeStore = AgeStoreState & AgeStoreActions const ageStore = createStore()((set) => ({ age: 42, setAge: (nextAge) => set((state) => ({ age: typeof nextAge === 'function' ? nextAge(state.age) : nextAge, })), })) function increment() { ageStore.getState().setAge((currentAge) => currentAge + 1) } const $yourAgeHeading = document.getElementById( 'your-age', ) as HTMLHeadingElement const $incrementBy3Button = document.getElementById( 'increment-by-3', ) as HTMLButtonElement const $incrementBy1Button = document.getElementById( 'increment-by-1', ) as HTMLButtonElement $incrementBy3Button.addEventListener('click', () => { increment() increment() increment() }) $incrementBy1Button.addEventListener('click', () => { increment() }) const render: Parameters[0] = (state) => { $yourAgeHeading.innerHTML = `Your age: ${state.age}` } render(ageStore.getInitialState(), ageStore.getInitialState()) ageStore.subscribe(render) ``` Here's the `html` code ```html

``` ### Updating Primitives in State State can hold any kind of JavaScript value. When you want to update built-in primitive values like numbers, strings, booleans, etc. you should directly assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. > [!NOTE] > By default, `set` function performs a shallow merge. If you need to completely replace > the state with a new one, use the `replace` parameter set to `true` ```ts import { createStore } from 'zustand/vanilla' type XStore = number const xStore = createStore()(() => 0) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { xStore.setState(event.clientX, true) }) const render: Parameters[0] = (x) => { $dot.style.transform = `translate(${x}px, 0)` } render(xStore.getInitialState(), xStore.getInitialState()) xStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Updating Objects in State Objects are **mutable** in JavaScript, but you should treat them as **immutable** when you store them in state. Instead, when you want to update an object, you need to create a new one (or make a copy of an existing one), and then set the state to use the new object. By default, `set` function performs a shallow merge. For most updates where you only need to modify specific properties, the default shallow merge is preferred as it's more efficient. To completely replace the state with a new one, use the `replace` parameter set to `true` with caution, as it discards any existing nested data within the state. ```ts import { createStore } from 'zustand/vanilla' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Updating Arrays in State Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array. By default, `set` function performs a shallow merge. To update array values we should assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. To completely replace the state with a new one, use the `replace` parameter set to `true`. > [!IMPORTANT] > We should prefer immutable operations like: `[...array]`, `concat(...)`, `filter(...)`, > `slice(...)`, `map(...)`, `toSpliced(...)`, `toSorted(...)`, and `toReversed(...)`, and avoid > mutable operations like `array[arrayIndex] = ...`, `push(...)`, `unshift(...)`, `pop(...)`, > `shift(...)`, `splice(...)`, `reverse(...)`, and `sort(...)`. ```ts import { createStore } from 'zustand/vanilla' type PositionStore = [number, number] const positionStore = createStore()(() => [0, 0]) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.setState([event.clientX, event.clientY], true) }) const render: Parameters[0] = ([x, y]) => { $dot.style.transform = `translate(${x}px, ${y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Subscribing to state updates By subscribing to state updates, you register a callback that fires whenever the store's state updates. We can use `subscribe` for external state management. ```ts import { createStore } from 'zustand/vanilla' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) const $dot = document.getElementById('dot') as HTMLDivElement $dot.addEventListener('mouseenter', (event) => { const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight positionStore.getState().setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe(render) const logger: Parameters[0] = (state) => { console.log('new position', { position: state.position }) } positionStore.subscribe(logger) ``` Here's the `html` code ```html
``` ## Troubleshooting ### I’ve updated the state, but the screen doesn’t update In the previous example, the `position` object is always created fresh from the current cursor position. But often, you will want to include existing data as a part of the new object you’re creating. For example, you may want to update only one field in a form, but keep the previous values for all other fields. These input fields don’t work because the `oninput` handlers mutate the state: ```ts import { createStore } from 'zustand/vanilla' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: (nextPerson: PersonStoreState['person']) => void } type PersonStore = PersonStoreState & PersonStoreActions const personStore = createStore()((set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (person) => set({ person }), })) const $firstNameInput = document.getElementById( 'first-name', ) as HTMLInputElement const $lastNameInput = document.getElementById('last-name') as HTMLInputElement const $emailInput = document.getElementById('email') as HTMLInputElement const $result = document.getElementById('result') as HTMLDivElement function handleFirstNameChange(event: Event) { personStore.getState().person.firstName = (event.target as any).value } function handleLastNameChange(event: Event) { personStore.getState().person.lastName = (event.target as any).value } function handleEmailChange(event: Event) { personStore.getState().person.email = (event.target as any).value } $firstNameInput.addEventListener('input', handleFirstNameChange) $lastNameInput.addEventListener('input', handleLastNameChange) $emailInput.addEventListener('input', handleEmailChange) const render: Parameters[0] = (state) => { $firstNameInput.value = state.person.firstName $lastNameInput.value = state.person.lastName $emailInput.value = state.person.email $result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})` } render(personStore.getInitialState(), personStore.getInitialState()) personStore.subscribe(render) ``` Here's the `html` code ```html

``` For example, this line mutates the state from a past render: ```ts personStore.getState().firstName = (e.target as any).value ``` The reliable way to get the behavior you’re looking for is to create a new object and pass it to `setPerson`. But here you want to also copy the existing data into it because only one of the fields has changed: ```ts personStore.getState().setPerson({ firstName: e.target.value, // New first name from the input }) ``` > [!NOTE] > We don’t need to copy every property separately due to `set` function performing shallow merge by > default. Now the form works! Notice how you didn’t declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient—as long as you update it correctly! ```ts {32-34,38-40,44-46} import { createStore } from 'zustand/vanilla' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: (nextPerson: PersonStoreState['person']) => void } type PersonStore = PersonStoreState & PersonStoreActions const personStore = createStore()((set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (person) => set({ person }), })) const $firstNameInput = document.getElementById( 'first-name', ) as HTMLInputElement const $lastNameInput = document.getElementById('last-name') as HTMLInputElement const $emailInput = document.getElementById('email') as HTMLInputElement const $result = document.getElementById('result') as HTMLDivElement function handleFirstNameChange(event: Event) { personStore.getState().setPerson({ ...personStore.getState().person, firstName: (event.target as any).value, }) } function handleLastNameChange(event: Event) { personStore.getState().setPerson({ ...personStore.getState().person, lastName: (event.target as any).value, }) } function handleEmailChange(event: Event) { personStore.getState().setPerson({ ...personStore.getState().person, email: (event.target as any).value, }) } $firstNameInput.addEventListener('input', handleFirstNameChange) $lastNameInput.addEventListener('input', handleLastNameChange) $emailInput.addEventListener('input', handleEmailChange) const render: Parameters[0] = (state) => { $firstNameInput.value = state.person.firstName $lastNameInput.value = state.person.lastName $emailInput.value = state.person.email $result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})` } render(personStore.getInitialState(), personStore.getInitialState()) personStore.subscribe(render) ``` ================================================ FILE: docs/reference/apis/create-with-equality-fn.md ================================================ --- title: createWithEqualityFn description: How to create efficient stores tag: react nav: 23 --- `createWithEqualityFn` lets you create a React Hook with API utilities attached, just like `create`. However, it offers a way to define a custom equality check. This allows for more granular control over when components re-render, improving performance and responsiveness. > [!IMPORTANT] > In order to use `createWithEqualityFn` from `zustand/traditional` you need to install > `use-sync-external-store` library due to `zustand/traditional` relies on `useSyncExternalStoreWithSelector`. ```js const useSomeStore = createWithEqualityFn(stateCreatorFn, equalityFn) ``` - [Types](#types) - [Signature](#signature) - [Reference](#reference) - [Usage](#usage) - [Updating state based on previous state](#updating-state-based-on-previous-state) - [Updating Primitives in State](#updating-primitives-in-state) - [Updating Objects in State](#updating-objects-in-state) - [Updating Arrays in State](#updating-arrays-in-state) - [Updating state with no store actions](#updating-state-with-no-store-actions) - [Subscribing to state updates](#subscribing-to-state-updates) - [Troubleshooting](#troubleshooting) - [I’ve updated the state, but the screen doesn’t update](#i’ve-updated-the-state,-but-the-screen-doesn’t-update) ## Types ### Signature ```ts createWithEqualityFn()(stateCreatorFn: StateCreator, equalityFn?: (a: T, b: T) => boolean): UseBoundStore> ``` ## Reference ### `createWithEqualityFn(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. - **optional** `equalityFn`: Defaults to `Object.is`. A function that lets you skip re-renders. #### Returns `createWithEqualityFn` returns a React Hook with API utilities attached, just like `create`. It lets you return data that is based on current state, using a selector function, and lets you skip re-renders using an equality function. It should take a selector function, and an equality function as arguments. ## Usage ### Updating state based on previous state To update a state based on previous state we should use **updater functions**. Read more about that [here](https://react.dev/learn/queueing-a-series-of-state-updates). This example shows how you can support **updater functions** within **actions**. ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type AgeStoreState = { age: number } type AgeStoreActions = { setAge: ( nextAge: | AgeStoreState['age'] | ((currentAge: AgeStoreState['age']) => AgeStoreState['age']), ) => void } type AgeStore = AgeStoreState & AgeStoreActions const useAgeStore = createWithEqualityFn()( (set) => ({ age: 42, setAge: (nextAge) => set((state) => ({ age: typeof nextAge === 'function' ? nextAge(state.age) : nextAge, })), }), shallow, ) export default function App() { const age = useAgeStore((state) => state.age) const setAge = useAgeStore((state) => state.setAge) function increment() { setAge((currentAge) => currentAge + 1) } return ( <>

Your age: {age}

) } ``` ### Updating Primitives in State State can hold any kind of JavaScript value. When you want to update built-in primitive values like numbers, strings, booleans, etc. you should directly assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. > [!NOTE] > By default, `set` function performs a shallow merge. If you need to completely replace > the state with a new one, use the `replace` parameter set to `true` ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type XStore = number const useXStore = createWithEqualityFn()(() => 0, shallow) export default function MovingDot() { const x = useXStore() const setX = (nextX: number) => { useXStore.setState(nextX, true) } const position = { y: 0, x } return (
{ setX(e.clientX) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating Objects in State Objects are **mutable** in JavaScript, but you should treat them as **immutable** when you store them in state. Instead, when you want to update an object, you need to create a new one (or make a copy of an existing one), and then set the state to use the new object. By default, `set` function performs a shallow merge. For most updates where you only need to modify specific properties, the default shallow merge is preferred as it's more efficient. To completely replace the state with a new one, use the `replace` parameter set to `true` with caution, as it discards any existing nested data within the state. ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const usePositionStore = createWithEqualityFn()( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), shallow, ) export default function MovingDot() { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating Arrays in State Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array. By default, `set` function performs a shallow merge. To update array values we should assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. To completely replace the state with a new one, use the `replace` parameter set to `true`. > [!IMPORTANT] > We should prefer immutable operations like: `[...array]`, `concat(...)`, `filter(...)`, > `slice(...)`, `map(...)`, `toSpliced(...)`, `toSorted(...)`, and `toReversed(...)`, and avoid > mutable operations like `array[arrayIndex] = ...`, `push(...)`, `unshift(...)`, `pop(...)`, > `shift(...)`, `splice(...)`, `reverse(...)`, and `sort(...)`. ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type PositionStore = [number, number] const usePositionStore = createWithEqualityFn()( () => [0, 0], shallow, ) export default function MovingDot() { const [x, y] = usePositionStore() const position = { x, y } const setPosition: typeof usePositionStore.setState = (nextPosition) => { usePositionStore.setState(nextPosition, true) } return (
{ setPosition([e.clientX, e.clientY]) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating state with no store actions Defining actions at module level, external to the store have a few advantages like: it doesn't require a hook to call an action, and it facilitates code splitting. > [!NOTE] > The recommended way is to colocate actions and states within the store (let your actions be > located together with your state). ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' const usePositionStore = createWithEqualityFn<{ x: number y: number }>()(() => ({ x: 0, y: 0 }), shallow) const setPosition: typeof usePositionStore.setState = (nextPosition) => { usePositionStore.setState(nextPosition) } export default function MovingDot() { const position = usePositionStore() return (
{ const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }} />
) } ``` ### Subscribing to state updates By subscribing to state updates, you register a callback that fires whenever the store's state updates. We can use `subscribe` for external state management. ```tsx import { useEffect } from 'react' import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const usePositionStore = createWithEqualityFn()( (set) => ({ position: { x: 0, y: 0 }, setPosition: (nextPosition) => set({ position: nextPosition }), }), shallow, ) export default function MovingDot() { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) useEffect(() => { const unsubscribePositionStore = usePositionStore.subscribe( ({ position }) => { console.log('new position', { position }) }, ) return () => { unsubscribePositionStore() } }, []) return (
{ const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }} />
) } ``` ## Troubleshooting ### I’ve updated the state, but the screen doesn’t update In the previous example, the `position` object is always created fresh from the current cursor position. But often, you will want to include existing data as a part of the new object you’re creating. For example, you may want to update only one field in a form, but keep the previous values for all other fields. These input fields don’t work because the `onChange` handlers mutate the state: ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: (nextPerson: PersonStoreState['person']) => void } type PersonStore = PersonStoreState & PersonStoreActions const usePersonStore = createWithEqualityFn()( (set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (person) => set({ person }), }), shallow, ) export default function Form() { const person = usePersonStore((state) => state.person) const setPerson = usePersonStore((state) => state.setPerson) function handleFirstNameChange(e: ChangeEvent) { person.firstName = e.target.value } function handleLastNameChange(e: ChangeEvent) { person.lastName = e.target.value } function handleEmailChange(e: ChangeEvent) { person.email = e.target.value } return ( <>

{person.firstName} {person.lastName} ({person.email})

) } ``` For example, this line mutates the state from a past render: ```tsx person.firstName = e.target.value ``` The reliable way to get the behavior you’re looking for is to create a new object and pass it to `setPerson`. But here you want to also copy the existing data into it because only one of the fields has changed: ```ts setPerson({ ...person, firstName: e.target.value }) // New first name from the input ``` > [!NOTE] > We don’t need to copy every property separately due to `set` function performing shallow merge by > default. Now the form works! Notice how you didn’t declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient—as long as you update it correctly! ```tsx {32,36,40} import { type ChangeEvent } from 'react' import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: (nextPerson: PersonStoreState['person']) => void } type PersonStore = PersonStoreState & PersonStoreActions const usePersonStore = createWithEqualityFn()( (set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (nextPerson) => set({ person: nextPerson }), }), shallow, ) export default function Form() { const person = usePersonStore((state) => state.person) const setPerson = usePersonStore((state) => state.setPerson) function handleFirstNameChange(e: ChangeEvent) { setPerson({ ...person, firstName: e.target.value }) } function handleLastNameChange(e: ChangeEvent) { setPerson({ ...person, lastName: e.target.value }) } function handleEmailChange(e: ChangeEvent) { setPerson({ ...person, email: e.target.value }) } return ( <>

{person.firstName} {person.lastName} ({person.email})

) } ``` ================================================ FILE: docs/reference/apis/create.md ================================================ --- title: create description: How to create stores tag: react nav: 21 --- `create` lets you create a React Hook with API utilities attached. ```js const useSomeStore = create(stateCreatorFn) ``` - [Types](#types) - [Signature](#signature) - [Reference](#reference) - [Usage](#usage) - [Updating state based on previous state](#updating-state-based-on-previous-state) - [Updating Primitives in State](#updating-primitives-in-state) - [Updating Objects in State](#updating-objects-in-state) - [Updating Arrays in State](#updating-arrays-in-state) - [Updating state with no store actions](#updating-state-with-no-store-actions) - [Subscribing to state updates](#subscribing-to-state-updates) - [Troubleshooting](#troubleshooting) - [I’ve updated the state, but the screen doesn’t update](#i’ve-updated-the-state,-but-the-screen-doesn’t-update) ## Types ### Signature ```ts create()(stateCreatorFn: StateCreator): UseBoundStore> ``` ## Reference ### `create(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. #### Returns `create` returns a React Hook with API utilities, `setState`, `getState`, `getInitialState` and `subscribe`, attached. It lets you return data that is based on current state, using a selector function. It should take a selector function as its only argument. ## Usage ### Updating state based on previous state To update a state based on previous state we should use **updater functions**. Read more about that [here](https://react.dev/learn/queueing-a-series-of-state-updates). This example shows how you can support **updater functions** within **actions**. ```tsx import { create } from 'zustand' type AgeStoreState = { age: number } type AgeStoreActions = { setAge: ( nextAge: | AgeStoreState['age'] | ((currentAge: AgeStoreState['age']) => AgeStoreState['age']), ) => void } type AgeStore = AgeStoreState & AgeStoreActions const useAgeStore = create()((set) => ({ age: 42, setAge: (nextAge) => { set((state) => ({ age: typeof nextAge === 'function' ? nextAge(state.age) : nextAge, })) }, })) export default function App() { const age = useAgeStore((state) => state.age) const setAge = useAgeStore((state) => state.setAge) function increment() { setAge((currentAge) => currentAge + 1) } return ( <>

Your age: {age}

) } ``` ### Updating Primitives in State State can hold any kind of JavaScript value. When you want to update built-in primitive values like numbers, strings, booleans, etc. you should directly assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. > [!NOTE] > By default, `set` function performs a shallow merge. If you need to completely replace the state > with a new one, use the `replace` parameter set to `true` ```tsx import { create } from 'zustand' type XStore = number const useXStore = create()(() => 0) export default function MovingDot() { const x = useXStore() const setX = (nextX: number) => { useXStore.setState(nextX, true) } const position = { y: 0, x } return (
{ setX(e.clientX) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating Objects in State Objects are **mutable** in JavaScript, but you should treat them as **immutable** when you store them in state. Instead, when you want to update an object, you need to create a new one (or make a copy of an existing one), and then set the state to use the new object. By default, `set` function performs a shallow merge. For most updates where you only need to modify specific properties, the default shallow merge is preferred as it's more efficient. To completely replace the state with a new one, use the `replace` parameter set to `true` with caution, as it discards any existing nested data within the state. ```tsx import { create } from 'zustand' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const usePositionStore = create()((set) => ({ position: { x: 0, y: 0 }, setPosition: (nextPosition) => set({ position: nextPosition }), })) export default function MovingDot() { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating Arrays in State Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array. By default, `set` function performs a shallow merge. To update array values we should assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. To completely replace the state with a new one, use the `replace` parameter set to `true`. > [!IMPORTANT] > We should prefer immutable operations like: `[...array]`, `concat(...)`, `filter(...)`, > `slice(...)`, `map(...)`, `toSpliced(...)`, `toSorted(...)`, and `toReversed(...)`, and avoid > mutable operations like `array[arrayIndex] = ...`, `push(...)`, `unshift(...)`, `pop(...)`, > `shift(...)`, `splice(...)`, `reverse(...)`, and `sort(...)`. ```tsx import { create } from 'zustand' type PositionStore = [number, number] const usePositionStore = create()(() => [0, 0]) export default function MovingDot() { const [x, y] = usePositionStore() const setPosition: typeof usePositionStore.setState = (nextPosition) => { usePositionStore.setState(nextPosition, true) } const position = { x, y } return (
{ setPosition([e.clientX, e.clientY]) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating state with no store actions Defining actions at module level, external to the store have a few advantages like: it doesn't require a hook to call an action, and it facilitates code splitting. > [!NOTE] > The recommended way is to colocate actions and states within the store (let your actions be > located together with your state). ```tsx import { create } from 'zustand' const usePositionStore = create<{ x: number y: number }>()(() => ({ x: 0, y: 0 })) const setPosition: typeof usePositionStore.setState = (nextPosition) => { usePositionStore.setState(nextPosition) } export default function MovingDot() { const position = usePositionStore() return (
{ const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }} />
) } ``` ### Subscribing to state updates By subscribing to state updates, you register a callback that fires whenever the store's state updates. We can use `subscribe` for external state management. ```tsx import { useEffect } from 'react' import { create } from 'zustand' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const usePositionStore = create()((set) => ({ position: { x: 0, y: 0 }, setPosition: (nextPosition) => set({ position: nextPosition }), })) export default function MovingDot() { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) useEffect(() => { const unsubscribePositionStore = usePositionStore.subscribe( ({ position }) => { console.log('new position', { position }) }, ) return () => { unsubscribePositionStore() } }, []) return (
{ const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }} />
) } ``` ## Troubleshooting ### I’ve updated the state, but the screen doesn’t update In the previous example, the `position` object is always created fresh from the current cursor position. But often, you will want to include existing data as a part of the new object you’re creating. For example, you may want to update only one field in a form, but keep the previous values for all other fields. These input fields don’t work because the `onChange` handlers mutate the state: ```tsx import { create } from 'zustand' type PersonStoreState = { firstName: string lastName: string email: string } type PersonStoreActions = { setPerson: (nextPerson: Partial) => void } type PersonStore = PersonStoreState & PersonStoreActions const usePersonStore = create()((set) => ({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', setPerson: (nextPerson) => set(nextPerson), })) export default function Form() { const person = usePersonStore((state) => state) const setPerson = usePersonStore((state) => state.setPerson) function handleFirstNameChange(e: ChangeEvent) { person.firstName = e.target.value } function handleLastNameChange(e: ChangeEvent) { person.lastName = e.target.value } function handleEmailChange(e: ChangeEvent) { person.email = e.target.value } return ( <>

{person.firstName} {person.lastName} ({person.email})

) } ``` For example, this line mutates the state from a past render: ```tsx person.firstName = e.target.value ``` The reliable way to get the behavior you’re looking for is to create a new object and pass it to `setPerson`. But here you want to also copy the existing data into it because only one of the fields has changed: ```ts setPerson({ ...person, firstName: e.target.value }) // New first name from the input ``` > [!NOTE] > We don’t need to copy every property separately due to `set` function performing shallow merge by > default. Now the form works! Notice how you didn’t declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient—as long as you update it correctly! ```tsx {27,31,35} import { create } from 'zustand' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: (nextPerson: PersonStoreState['person']) => void } type PersonStore = PersonStoreState & PersonStoreActions const usePersonStore = create()((set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (nextPerson) => set(nextPerson), })) export default function Form() { const person = usePersonStore((state) => state.person) const setPerson = usePersonStore((state) => state.setPerson) function handleFirstNameChange(e: ChangeEvent) { setPerson({ ...person, firstName: e.target.value }) } function handleLastNameChange(e: ChangeEvent) { setPerson({ ...person, lastName: e.target.value }) } function handleEmailChange(e: ChangeEvent) { setPerson({ ...person, email: e.target.value }) } return ( <>

{person.firstName} {person.lastName} ({person.email})

) } ``` ================================================ FILE: docs/reference/apis/shallow.md ================================================ --- title: shallow description: How compare simple data effectively nav: 24 --- `shallow` lets you run fast checks on simple data structures. It effectively identifies changes in **top-level** properties when you're working with data structures that don't have nested objects or arrays within them. > [!NOTE] > Shallow lets you perform quick comparisons, but keep its limitations in mind. ```js const equal = shallow(a, b) ``` - [Types](#types) - [Signature](#signature) - [Reference](#reference) - [Usage](#usage) - [Comparing Primitives](#comparing-primitives) - [Comparing Objects](#comparing-objects) - [Comparing Sets](#comparing-sets) - [Comparing Maps](#comparing-maps) - [Troubleshooting](#troubleshooting) - [Comparing objects returns `false` even if they are identical](#comparing-objects-returns-false-even-if-they-are-identical) - [Comparing objects with different prototypes](#comparing-objects-with-different-prototypes) ## Types ### Signature ```ts shallow(a: T, b: T): boolean ``` ## Reference ### `shallow(a, b)` #### Parameters - `a`: The first value. - `b`: The second value. #### Returns `shallow` returns `true` when `a` and `b` are equal based on a shallow comparison of their **top-level** properties. Otherwise, it should return `false`. ## Usage ### Comparing Primitives When comparing primitive values like `string`s, `number`s, `boolean`s, and `BigInt`s, both `Object.is` and `shallow` function return `true` if the values are the same. This is because primitive values are compared by their actual value rather than by reference. ```ts const stringLeft = 'John Doe' const stringRight = 'John Doe' Object.is(stringLeft, stringRight) // -> true shallow(stringLeft, stringRight) // -> true const numberLeft = 10 const numberRight = 10 Object.is(numberLeft, numberRight) // -> true shallow(numberLeft, numberRight) // -> true const booleanLeft = true const booleanRight = true Object.is(booleanLeft, booleanRight) // -> true shallow(booleanLeft, booleanRight) // -> true const bigIntLeft = 1n const bigIntRight = 1n Object.is(bigIntLeft, bigIntRight) // -> true shallow(bigIntLeft, bigIntRight) // -> true ``` ### Comparing Objects When comparing objects, it's important to understand how `Object.is` and `shallow` function operate, as they handle comparisons differently. The `shallow` function returns `true` because shallow performs a shallow comparison of the objects. It checks if the top-level properties and their values are the same. In this case, the top-level properties (`firstName`, `lastName`, and `age`) and their values are identical between `objectLeft` and `objectRight`, so shallow considers them equal. ```ts const objectLeft = { firstName: 'John', lastName: 'Doe', age: 30, } const objectRight = { firstName: 'John', lastName: 'Doe', age: 30, } Object.is(objectLeft, objectRight) // -> false shallow(objectLeft, objectRight) // -> true ``` ### Comparing Sets When comparing sets, it's important to understand how `Object.is` and `shallow` function operate, as they handle comparisons differently. The `shallow` function returns `true` because shallow performs a shallow comparison of the sets. It checks if the top-level properties (in this case, the sets themselves) are the same. Since `setLeft` and `setRight` are both instances of the Set object and contain the same elements, shallow considers them equal. ```ts const setLeft = new Set([1, 2, 3]) const setRight = new Set([1, 2, 3]) Object.is(setLeft, setRight) // -> false shallow(setLeft, setRight) // -> true ``` ### Comparing Maps When comparing maps, it's important to understand how `Object.is` and `shallow` function operate, as they handle comparisons differently. The `shallow` returns `true` because shallow performs a shallow comparison of the maps. It checks if the top-level properties (in this case, the maps themselves) are the same. Since `mapLeft` and `mapRight` are both instances of the Map object and contain the same key-value pairs, shallow considers them equal. ```ts const mapLeft = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]) const mapRight = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]) Object.is(mapLeft, mapRight) // -> false shallow(mapLeft, mapRight) // -> true ``` ## Troubleshooting ### Comparing objects returns `false` even if they are identical The `shallow` function performs a shallow comparison. A shallow comparison checks if the top-level properties of two objects are equal. It does not check nested objects or deeply nested properties. In other words, it only compares the references of the properties. In the following example, the shallow function returns `false` because it compares only the top-level properties and their references. The address property in both objects is a nested object, and even though their contents are identical, their references are different. Consequently, shallow sees them as different, resulting in `false`. ```ts const objectLeft = { firstName: 'John', lastName: 'Doe', age: 30, address: { street: 'Kulas Light', suite: 'Apt. 556', city: 'Gwenborough', zipcode: '92998-3874', geo: { lat: '-37.3159', lng: '81.1496', }, }, } const objectRight = { firstName: 'John', lastName: 'Doe', age: 30, address: { street: 'Kulas Light', suite: 'Apt. 556', city: 'Gwenborough', zipcode: '92998-3874', geo: { lat: '-37.3159', lng: '81.1496', }, }, } Object.is(objectLeft, objectRight) // -> false shallow(objectLeft, objectRight) // -> false ``` If we remove the `address` property, the shallow comparison would work as expected because all top-level properties would be primitive values or references to the same values: ```ts const objectLeft = { firstName: 'John', lastName: 'Doe', age: 30, } const objectRight = { firstName: 'John', lastName: 'Doe', age: 30, } Object.is(objectLeft, objectRight) // -> false shallow(objectLeft, objectRight) // -> true ``` In this modified example, `objectLeft` and `objectRight` have the same top-level properties and primitive values. Since `shallow` function only compares the top-level properties, it will return `true` because the primitive values (`firstName`, `lastName`, and `age`) are identical in both objects. ### Comparing objects with different prototypes The `shallow` function checks whether the two objects have the same prototype. If their prototypes are referentially different, shallow will return `false`. This comparison is done using: ```ts Object.getPrototypeOf(a) === Object.getPrototypeOf(b) ``` > [!IMPORTANT] > Objects created with the object initializer (`{}`) or with `new Object()` inherit from > `Object.prototype` by default. However, objects created with `Object.create(proto)` inherit from > the proto you pass in—which may not be `Object.prototype.` ```ts const a = Object.create({}) // -> prototype is `{}` const b = {} // -> prototype is `Object.prototype` shallow(a, b) // -> false ``` ================================================ FILE: docs/reference/hooks/use-shallow.md ================================================ --- title: useShallow description: How to memoize selector functions tag: react nav: 27 --- `useShallow` is a React Hook that lets you optimize re-renders. ```js const memoizedSelector = useShallow(selector) ``` - [Types](#types) - [Signature](#signature) - [Reference](#reference) - [Usage](#usage) - [Writing a memoized selector](#writing-a-memoized-selector) - [Troubleshooting](#troubleshooting) ## Types ### Signature ```ts useShallow(selectorFn: (state: T) => U): (state: T) => U ``` ## Reference ### `useShallow(selectorFn)` #### Parameters - `selectorFn`: A function that lets you return data that is based on current state. #### Returns `useShallow` returns a memoized version of a selector function using a shallow comparison for memoization. ## Usage ### Writing a memoized selector First, we need to setup a store to hold the state for the bear family. In this store, we define three properties: `papaBear`, `mamaBear`, and `babyBear`, each representing a different member of the bear family and their respective oatmeal pot sizes. ```tsx import { create } from 'zustand' type BearFamilyMealsStore = { [key: string]: string } const useBearFamilyMealsStore = create()(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', babyBear: 'A little, small, wee pot', })) ``` Next, we'll create a `BearNames` component that retrieves the keys of our state (the bear family members) and displays them. ```tsx function BearNames() { const names = useBearFamilyMealsStore((state) => Object.keys(state)) return
{names.join(', ')}
} ``` Next, we will create a `UpdateBabyBearMeal` component that periodically updates baby bear's meal choice. ```tsx const meals = [ 'A tiny, little, wee bowl', 'A small, petite, tiny pot', 'A wee, itty-bitty, small bowl', 'A little, petite, tiny dish', 'A tiny, small, wee vessel', 'A small, little, wee cauldron', 'A little, tiny, small cup', 'A wee, small, little jar', 'A tiny, wee, small pan', 'A small, wee, little crock', ] function UpdateBabyBearMeal() { useEffect(() => { const timer = setInterval(() => { useBearFamilyMealsStore.setState({ babyBear: meals[Math.floor(Math.random() * (meals.length - 1))], }) }, 1000) return () => { clearInterval(timer) } }, []) return null } ``` Finally, we combine both components in the `App` component to see them in action. ```tsx export default function App() { return ( <> ) } ``` Here is what the code should look like: ```tsx import { useEffect } from 'react' import { create } from 'zustand' type BearFamilyMealsStore = { [key: string]: string } const useBearFamilyMealsStore = create()(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', babyBear: 'A little, small, wee pot', })) const meals = [ 'A tiny, little, wee bowl', 'A small, petite, tiny pot', 'A wee, itty-bitty, small bowl', 'A little, petite, tiny dish', 'A tiny, small, wee vessel', 'A small, little, wee cauldron', 'A little, tiny, small cup', 'A wee, small, little jar', 'A tiny, wee, small pan', 'A small, wee, little crock', ] function UpdateBabyBearMeal() { useEffect(() => { const timer = setInterval(() => { useBearFamilyMealsStore.setState({ babyBear: meals[Math.floor(Math.random() * (meals.length - 1))], }) }, 1000) return () => { clearInterval(timer) } }, []) return null } function BearNames() { const names = useBearFamilyMealsStore((state) => Object.keys(state)) return
{names.join(', ')}
} export default function App() { return ( <> ) } ``` Everything might look fine, but there’s a small problem: the `BearNames` component keeps re-rendering even if the names haven’t changed. This happens because the component re-renders whenever any part of the state changes, even if the specific part we care about (the list of names) hasn’t changed. To fix this, we use `useShallow` to make sure the component only re-renders when the actual keys of the state change: ```tsx function BearNames() { const names = useBearFamilyMealsStore( useShallow((state) => Object.keys(state)), ) return
{names.join(', ')}
} ``` Here is what the code should look like: ```tsx import { useEffect } from 'react' import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' type BearFamilyMealsStore = { [key: string]: string } const useBearFamilyMealsStore = create()(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', babyBear: 'A little, small, wee pot', })) const meals = [ 'A tiny, little, wee bowl', 'A small, petite, tiny pot', 'A wee, itty-bitty, small bowl', 'A little, petite, tiny dish', 'A tiny, small, wee vessel', 'A small, little, wee cauldron', 'A little, tiny, small cup', 'A wee, small, little jar', 'A tiny, wee, small pan', 'A small, wee, little crock', ] function UpdateBabyBearMeal() { useEffect(() => { const timer = setInterval(() => { useBearFamilyMealsStore.setState({ babyBear: meals[Math.floor(Math.random() * (meals.length - 1))], }) }, 1000) return () => { clearInterval(timer) } }, []) return null } function BearNames() { const names = useBearFamilyMealsStore( useShallow((state) => Object.keys(state)), ) return
{names.join(', ')}
} export default function App() { return ( <> ) } ``` By using `useShallow`, we optimized the rendering process, ensuring that the component only re-renders when necessary, which improves overall performance. ## Troubleshooting TBD ================================================ FILE: docs/reference/hooks/use-store-with-equality-fn.md ================================================ --- title: useStoreWithEqualityFn description: How to use vanilla stores effectively in React tag: react nav: 26 --- `useStoreWithEqualityFn` is a React Hook that lets you use a vanilla store in React, just like `useStore`. However, it offers a way to define a custom equality check. This allows for more granular control over when components re-render, improving performance and responsiveness. > [!IMPORTANT] > In order to use `useStoreWithEqualityFn` from `zustand/traditional` you need to install > `use-sync-external-store` library due to `zustand/traditional` relies on `useSyncExternalStoreWithSelector`. ```js const someState = useStoreWithEqualityFn(store, selectorFn, equalityFn) ``` - [Types](#types) - [Signature](#signature) - [Reference](#reference) - [Usage](#usage) - [Using a global vanilla store in React](#using-a-global-vanilla-store-in-react) - [Using dynamic vanilla stores in React](#using-dynamic-global-vanilla-stores-in-react) - [Using scoped (non-global) vanilla store in React](<#using-scoped-(non-global)-vanilla-store-in-react>) - [Using dynamic scoped (non-global) vanilla stores in React](<#using-dynamic-scoped-(non-global)-vanilla-stores-in-react>) - [Troubleshooting](#troubleshooting) ## Types ### Signature ```ts useStoreWithEqualityFn(store: StoreApi, selectorFn: (state: T) => U, equalityFn?: (a: T, b: T) => boolean): U ``` ## Reference ### `useStoreWithEqualityFn(store, selectorFn, equalityFn)` #### Parameters - `storeApi`: The instance that lets you access to store API utilities. - `selectorFn`: A function that lets you return data that is based on current state. - `equalityFn`: A function that lets you skip re-renders. #### Returns `useStoreWithEqualityFn` returns any data based on current state depending on the selector function, and lets you skip re-renders using an equality function. It should take a store, a selector function, and an equality function as arguments. ## Usage ### Using a global vanilla store in React First, let's set up a store that will hold the position of the dot on the screen. We'll define the store to manage `x` and `y` coordinates and provide an action to update these coordinates. ```tsx import { createStore, useStore } from 'zustand' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) ``` Next, we'll create a `MovingDot` component that renders a div representing the dot. This component will use the store to track and update the dot's position. ```tsx function MovingDot() { const position = useStoreWithEqualityFn( positionStore, (state) => state.position, shallow, ) const setPosition = useStoreWithEqualityFn( positionStore, (state) => state.setPosition, shallow, ) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` Finally, we’ll render the `MovingDot` component in our `App` component. ```tsx export default function App() { return } ``` Here is what the code should look like: ```tsx import { createStore } from 'zustand' import { useStoreWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) function MovingDot() { const position = useStoreWithEqualityFn( positionStore, (state) => state.position, shallow, ) const setPosition = useStoreWithEqualityFn( positionStore, (state) => state.setPosition, shallow, ) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } export default function App() { return } ``` ### Using dynamic global vanilla stores in React First, we'll create a factory function that generates a store for managing the counter state. Each tab will have its own instance of this store. ```ts import { createStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } ``` Next, we'll create a factory function that manages the creation and retrieval of counter stores. This allows each tab to have its own independent counter. ```ts const defaultCounterStores = new Map< string, ReturnType >() const createCounterStoreFactory = ( counterStores: typeof defaultCounterStores, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const getOrCreateCounterStoreByKey = createCounterStoreFactory(defaultCounterStores) ``` Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s counter. ```tsx const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useStoreWithEqualityFn( getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), (state) => state, shallow, ) return (
Content of Tab {currentTabIndex + 1}

) ``` Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. ```tsx export default function App() { return } ``` Here is what the code should look like: ```tsx import { useState } from 'react' import { createStore } from 'zustand' import { useStoreWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const defaultCounterStores = new Map< string, ReturnType >() const createCounterStoreFactory = ( counterStores: typeof defaultCounterStores, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const getOrCreateCounterStoreByKey = createCounterStoreFactory(defaultCounterStores) export default function App() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useStoreWithEqualityFn( getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), (state) => state, shallow, ) return (
Content of Tab {currentTabIndex + 1}

) } ``` ### Using scoped (non-global) vanilla store in React First, let's set up a store that will hold the position of the dot on the screen. We'll define the store to manage `x` and `y` coordinates and provide an action to update these coordinates. ```tsx type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const createPositionStore = () => { return createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) } ``` Next, we'll create a context and a provider component to pass down the store through the React component tree. This allows each `MovingDot` component to have its own independent state. ```tsx const PositionStoreContext = createContext | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [store] = useState(() => createPositionStore()) return ( {children} ) } ``` To simplify accessing the store, we’ll create a React custom hook, `usePositionStore`. This hook will read the store from the context and allow us to select specific parts of the state. ```ts function usePositionStore(selector: (state: PositionStore) => U) { const store = useContext(PositionStoreContext) if (store === null) { throw new Error( 'usePositionStore must be used within PositionStoreProvider', ) } return useStoreWithEqualityFn(store, selector, shallow) } ``` Now, let's create the `MovingDot` component, which will render a dot that follows the mouse cursor within its container. ```tsx function MovingDot({ color }: { color: string }) { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} >
) } ``` Finally, we'll bring everything together in the `App` component, where we render two `MovingDot` components, each with its own independent state. ```tsx export default function App() { return (
) } ``` Here is what the code should look like: ```tsx import { type ReactNode, useState, createContext, useContext } from 'react' import { createStore } from 'zustand' import { useStoreWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const createPositionStore = () => { return createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) } const PositionStoreContext = createContext | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [store] = useState(() => createPositionStore()) return ( {children} ) } function usePositionStore(selector: (state: PositionStore) => U) { const store = useContext(PositionStoreContext) if (store === null) { throw new Error( 'usePositionStore must be used within PositionStoreProvider', ) } return useStoreWithEqualityFn(store, selector, shallow) } function MovingDot({ color }: { color: string }) { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} >
) } export default function App() { return (
) } ``` ### Using dynamic scoped (non-global) vanilla stores in React First, we'll create a factory function that generates a store for managing the counter state. Each tab will have its own instance of this store. ```ts type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } ``` Next, we'll create a factory function that manages the creation and retrieval of counter stores. This allows each tab to have its own independent counter. ```ts const createCounterStoreFactory = ( counterStores: Map>, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } ``` Next, we need a way to manage and access these stores throughout our app. We’ll use React’s context for this. ```tsx const CounterStoresContext = createContext(null) const CounterStoresProvider = ({ children }) => { const [stores] = useState( () => new Map>(), ) return ( {children} ) } ``` Now, we’ll create a custom hook, `useCounterStore`, that lets us access the correct store for a given tab. ```tsx const useCounterStore = ( key: string, selector: (state: CounterStore) => U, ) => { const stores = useContext(CounterStoresContext) if (stores === undefined) { throw new Error('useCounterStore must be used within CounterStoresProvider') } const getOrCreateCounterStoreByKey = useCallback( (key: string) => createCounterStoreFactory(stores!)(key), [stores], ) return useStore(getOrCreateCounterStoreByKey(key), selector) } ``` Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s counter. ```tsx function Tabs() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useCounterStore( `tab-${currentTabIndex}`, (state) => state, ) return (
Content of Tab {currentTabIndex + 1}

) } ``` Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. ```tsx export default function App() { return ( ) } ``` Here is what the code should look like: ```tsx import { type ReactNode, useState, useCallback, useContext, createContext, } from 'react' import { createStore, useStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const createCounterStoreFactory = ( counterStores: Map>, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const CounterStoresContext = createContext > | null>(null) const CounterStoresProvider = ({ children }: { children: ReactNode }) => { const [stores] = useState( () => new Map>(), ) return ( {children} ) } const useCounterStore = ( key: string, selector: (state: CounterStore) => U, ) => { const stores = useContext(CounterStoresContext) if (stores === undefined) { throw new Error('useCounterStore must be used within CounterStoresProvider') } const getOrCreateCounterStoreByKey = useCallback( (key: string) => createCounterStoreFactory(stores!)(key), [stores], ) return useStore(getOrCreateCounterStoreByKey(key), selector) } function Tabs() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useCounterStore( `tab-${currentTabIndex}`, (state) => state, ) return (
Content of Tab {currentTabIndex + 1}

) } export default function App() { return ( ) } ``` ## Troubleshooting TBD ================================================ FILE: docs/reference/hooks/use-store.md ================================================ --- title: useStore description: How to use vanilla stores in React tag: react nav: 25 --- `useStore` is a React Hook that lets you use a vanilla store in React. ```js const someState = useStore(store, selectorFn) ``` - [Types](#types) - [Signature](#signature) - [Reference](#reference) - [Usage](#usage) - [Use a vanilla store in React](#using-a-global-vanilla-store-in-react) - [Using dynamic vanilla stores in React](#using-dynamic-global-vanilla-stores-in-react) - [Using scoped (non-global) vanilla store in React](<#using-scoped-(non-global)-vanilla-store-in-react>) - [Using dynamic scoped (non-global) vanilla stores in React](<#using-dynamic-scoped-(non-global)-vanilla-stores-in-react>) - [Troubleshooting](#troubleshooting) ## Types ### Signature ```ts useStore, U = T>(store: StoreApi, selectorFn?: (state: T) => U) => UseBoundStore> ``` ## Reference ### `useStore(store, selectorFn)` #### Parameters - `storeApi`: The instance that lets you access to store API utilities. - `selectorFn`: A function that lets you return data that is based on current state. #### Returns `useStore` returns any data based on current state depending on the selector function. It should take a store, and a selector function as arguments. ## Usage ### Using a global vanilla store in React First, let's set up a store that will hold the position of the dot on the screen. We'll define the store to manage `x` and `y` coordinates and provide an action to update these coordinates. ```tsx type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) ``` Next, we'll create a `MovingDot` component that renders a div representing the dot. This component will use the store to track and update the dot's position. ```tsx function MovingDot() { const position = useStore(positionStore, (state) => state.position) const setPosition = useStore(positionStore, (state) => state.setPosition) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` Finally, we’ll render the `MovingDot` component in our `App` component. ```tsx export default function App() { return } ``` Here is what the code should look like: ```tsx import { createStore, useStore } from 'zustand' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) function MovingDot() { const position = useStore(positionStore, (state) => state.position) const setPosition = useStore(positionStore, (state) => state.setPosition) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } export default function App() { return } ``` ### Using dynamic global vanilla stores in React First, we'll create a factory function that generates a store for managing the counter state. Each tab will have its own instance of this store. ```ts type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } ``` Next, we'll create a factory function that manages the creation and retrieval of counter stores. This allows each tab to have its own independent counter. ```ts const defaultCounterStores = new Map< string, ReturnType >() const createCounterStoreFactory = ( counterStores: typeof defaultCounterStores, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const getOrCreateCounterStoreByKey = createCounterStoreFactory(defaultCounterStores) ``` Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s counter. ```tsx const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useStore( getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), ) return (
Content of Tab {currentTabIndex + 1}

) ``` Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. ```tsx export default function App() { return } ``` Here is what the code should look like: ```tsx import { useState } from 'react' import { createStore, useStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const defaultCounterStores = new Map< string, ReturnType >() const createCounterStoreFactory = ( counterStores: typeof defaultCounterStores, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const getOrCreateCounterStoreByKey = createCounterStoreFactory(defaultCounterStores) export default function App() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useStore( getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), ) return (
Content of Tab {currentTabIndex + 1}

) } ``` ### Using scoped (non-global) vanilla store in React First, let's set up a store that will hold the position of the dot on the screen. We'll define the store to manage `x` and `y` coordinates and provide an action to update these coordinates. ```tsx type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const createPositionStore = () => { return createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) } ``` Next, we'll create a context and a provider component to pass down the store through the React component tree. This allows each `MovingDot` component to have its own independent state. ```tsx const PositionStoreContext = createContext | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [store] = useState(() => createPositionStore()) return ( {children} ) } ``` To simplify accessing the store, we’ll create a React custom hook, `usePositionStore`. This hook will read the store from the context and allow us to select specific parts of the state. ```ts function usePositionStore(selector: (state: PositionStore) => U) { const store = useContext(PositionStoreContext) if (store === null) { throw new Error( 'usePositionStore must be used within PositionStoreProvider', ) } return useStore(store, selector) } ``` Now, let's create the `MovingDot` component, which will render a dot that follows the mouse cursor within its container. ```tsx function MovingDot({ color }: { color: string }) { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} >
) } ``` Finally, we'll bring everything together in the `App` component, where we render two `MovingDot` components, each with its own independent state. ```tsx export default function App() { return (
) } ``` Here is what the code should look like: ```tsx import { type ReactNode, useState, createContext, useContext } from 'react' import { createStore, useStore } from 'zustand' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const createPositionStore = () => { return createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) } const PositionStoreContext = createContext | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [store] = useState(() => createPositionStore()) return ( {children} ) } function usePositionStore(selector: (state: PositionStore) => U) { const store = useContext(PositionStoreContext) if (store === null) { throw new Error( 'usePositionStore must be used within PositionStoreProvider', ) } return useStore(store, selector) } function MovingDot({ color }: { color: string }) { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} >
) } export default function App() { return (
) } ``` ### Using dynamic scoped (non-global) vanilla stores in React First, we'll create a factory function that generates a store for managing the counter state. Each tab will have its own instance of this store. ```ts import { createStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } ``` Next, we'll create a factory function that manages the creation and retrieval of counter stores. This allows each tab to have its own independent counter. ```ts const createCounterStoreFactory = ( counterStores: Map>, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } ``` Next, we need a way to manage and access these stores throughout our app. We’ll use React’s context for this. ```tsx const CounterStoresContext = createContext(null) const CounterStoresProvider = ({ children }) => { const [stores] = useState( () => new Map>(), ) return ( {children} ) } ``` Now, we’ll create a custom hook, `useCounterStore`, that lets us access the correct store for a given tab. ```tsx const useCounterStore = ( currentTabIndex: number, selector: (state: CounterStore) => U, ) => { const stores = useContext(CounterStoresContext) if (stores === undefined) { throw new Error('useCounterStore must be used within CounterStoresProvider') } const getOrCreateCounterStoreByKey = useCallback( () => createCounterStoreFactory(stores), [stores], ) return useStore(getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`)) } ``` Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s counter. ```tsx function Tabs() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useCounterStore( `tab-${currentTabIndex}`, (state) => state, ) return (
Content of Tab {currentTabIndex + 1}

) } ``` Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. ```tsx export default function App() { return ( ) } ``` Here is what the code should look like: ```tsx import { type ReactNode, useState, useCallback, useContext, createContext, } from 'react' import { createStore, useStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const createCounterStoreFactory = ( counterStores: Map>, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const CounterStoresContext = createContext > | null>(null) const CounterStoresProvider = ({ children }: { children: ReactNode }) => { const [stores] = useState( () => new Map>(), ) return ( {children} ) } const useCounterStore = ( key: string, selector: (state: CounterStore) => U, ) => { const stores = useContext(CounterStoresContext) if (stores === undefined) { throw new Error('useCounterStore must be used within CounterStoresProvider') } const getOrCreateCounterStoreByKey = useCallback( (key: string) => createCounterStoreFactory(stores!)(key), [stores], ) return useStore(getOrCreateCounterStoreByKey(key), selector) } function Tabs() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useCounterStore( `tab-${currentTabIndex}`, (state) => state, ) return (
Content of Tab {currentTabIndex + 1}

) } export default function App() { return ( ) } ``` ## Troubleshooting TBD ================================================ FILE: docs/reference/index.md ================================================ --- title: Reference description: API-first reference for stores, hooks, middlewares, and integrations. --- ## APIs Core functions for creating and configuring stores. - [`create`](./apis/create.md) — Create a store bound to React via hooks. - [`createStore`](./apis/create-store.md) — Create a standalone store without React. - [`createWithEqualityFn`](./apis/create-with-equality-fn.md) — Like `create`, but with a custom equality function. - [`shallow`](./apis/shallow.md) — Utility for shallow comparison of objects and arrays. ## Hooks React hooks for reading and subscribing to store state. - [`useStore`](./hooks/use-store.md) — Access and subscribe to a vanilla store from a React component. - [`useStoreWithEqualityFn`](./hooks/use-store-with-equality-fn.md) — Like `useStore`, but with a custom equality function. - [`useShallow`](./hooks/use-shallow.md) — Derive a stable reference from a selector using shallow comparison. ## Middlewares Composable middleware functions for extending store behavior. - [`persist`](./middlewares/persist.md) — Persist and rehydrate state using `localStorage` or a custom storage engine. - [`devtools`](./middlewares/devtools.md) — Connect a store to Redux DevTools for time-travel debugging. - [`redux`](./middlewares/redux.md) — Use a reducer and dispatch pattern similar to Redux. - [`immer`](./middlewares/immer.md) — Write state updates with mutable syntax using Immer. - [`combine`](./middlewares/combine.md) — Combine separate state slices into a single store with inferred types. - [`subscribeWithSelector`](./middlewares/subscribe-with-selector.md) — Subscribe to a slice of state with selector and equality support. ## Integrations In-depth guides for using Zustand alongside third-party libraries. - [Persisting store data](./integrations/persisting-store-data.md) — Detailed guide to the `persist` middleware and storage adapters. - [Immer middleware](./integrations/immer-middleware.md) — Detailed guide to the `immer` middleware. - [Third-party libraries](./integrations/third-party-libraries.md) — Using Zustand with other libraries in the ecosystem. ## Migrations Upgrade guides between major versions. - [Migrating to v5](./migrations/migrating-to-v5.md) — How to upgrade from Zustand v4. - [Migrating to v4](./migrations/migrating-to-v4.md) — How to upgrade from Zustand v3. ## Previous versions APIs that existed in older versions of Zustand and are no longer recommended for new code. - [createContext (v3)](./previous-versions/zustand-v3-create-context.md) — The `createContext` export from `zustand/context`, deprecated in v4 and removed in v5. ================================================ FILE: docs/reference/integrations/immer-middleware.md ================================================ --- title: Immer middleware nav: 35 --- The [Immer](https://github.com/immerjs/immer) middleware enables you to use immutable state in a more convenient way. Also, with Immer, you can simplify handling immutable data structures in Zustand. ## Installation In order to use the Immer middleware in Zustand, you will need to install Immer as a direct dependency. ```bash npm install immer ``` ## Usage (Notice the extra parentheses after the type parameter as mentioned in the [Advanced Typescript Guide](../../learn/guides/advanced-typescript.md)). Updating simple states ```ts import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' type State = { count: number } type Actions = { increment: (qty: number) => void decrement: (qty: number) => void } export const useCountStore = create()( immer((set) => ({ count: 0, increment: (qty: number) => set((state) => { state.count += qty }), decrement: (qty: number) => set((state) => { state.count -= qty }), })), ) ``` Updating complex states ```ts import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' interface Todo { id: string title: string done: boolean } type State = { todos: Record } type Actions = { toggleTodo: (todoId: string) => void } export const useTodoStore = create()( immer((set) => ({ todos: { '82471c5f-4207-4b1d-abcb-b98547e01a3e': { id: '82471c5f-4207-4b1d-abcb-b98547e01a3e', title: 'Learn Zustand', done: false, }, '354ee16c-bfdd-44d3-afa9-e93679bda367': { id: '354ee16c-bfdd-44d3-afa9-e93679bda367', title: 'Learn Jotai', done: false, }, '771c85c5-46ea-4a11-8fed-36cc2c7be344': { id: '771c85c5-46ea-4a11-8fed-36cc2c7be344', title: 'Learn Valtio', done: false, }, '363a4bac-083f-47f7-a0a2-aeeee153a99c': { id: '363a4bac-083f-47f7-a0a2-aeeee153a99c', title: 'Learn Signals', done: false, }, }, toggleTodo: (todoId: string) => set((state) => { state.todos[todoId].done = !state.todos[todoId].done }), })), ) ``` ## Gotchas In this section you will find some things that you need to keep in mind when using Zustand with Immer. ### My subscriptions aren't being called If you are using Immer, make sure you are actually following [the rules of Immer](https://immerjs.github.io/immer/pitfalls). For example, you have to add `[immerable] = true` for [class objects](https://immerjs.github.io/immer/complex-objects) to work. If you don't do this, Immer will still mutate the object, but not as a proxy, so it will also update the current state. Zustand checks if the state has actually changed, so since both the current state and the next state are equal (if you don't do it correctly), Zustand will skip calling the subscriptions. ## Demos - Basic: https://stackblitz.com/edit/vitejs-vite-3sgc4ejy - Advanced: https://stackblitz.com/edit/vitejs-vite-jxxtuyj3 ================================================ FILE: docs/reference/integrations/persisting-store-data.md ================================================ --- title: Persisting store data nav: 34 --- The Persist middleware enables you to store your Zustand state in a storage (e.g., `localStorage`, `AsyncStorage`, `IndexedDB`, etc.), thus persisting its data. Note that this middleware supports both synchronous storages, like `localStorage`, and asynchronous storages, like `AsyncStorage`, but using an asynchronous storage does come with a cost. See [Hydration and asynchronous storages](#hydration-and-asynchronous-storages) for more details. ## Simple example ```ts import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' export const useBearStore = create()( persist( (set, get) => ({ bears: 0, addABear: () => set({ bears: get().bears + 1 }), }), { name: 'food-storage', // name of the item in the storage (must be unique) storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used }, ), ) ``` ## Typescript simple example ```ts import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' type BearStore = { bears: number addABear: () => void } export const useBearStore = create()( persist( (set, get) => ({ bears: 0, addABear: () => set({ bears: get().bears + 1 }), }), { name: 'food-storage', // name of the item in the storage (must be unique) storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used }, ), ) ``` ## Options ### `name` This is the only required option. The given name is going to be the key used to store your Zustand state in the storage, so it must be unique. ### `storage` > Type: `() => StateStorage` The `StateStorage` can be imported with: ```ts import { StateStorage } from 'zustand/middleware' ``` > Default: `createJSONStorage(() => localStorage)` Enables you to use your own storage. Simply pass a function that returns the storage you want to use. It's recommended to use the [`createJSONStorage`](#createjsonstorage) helper function to create a `storage` object that is compliant with the `StateStorage` interface. Example: ```ts import { persist, createJSONStorage } from 'zustand/middleware' export const useBoundStore = create( persist( (set, get) => ({ // ... }), { // ... storage: createJSONStorage(() => AsyncStorage), }, ), ) ``` ### `partialize` > Type: `(state: Object) => Object` > Default: `(state) => state` Enables you to pick some of the state's fields to be stored in the storage. You could omit multiple fields using the following: ```ts export const useBoundStore = create( persist( (set, get) => ({ foo: 0, bar: 1, }), { // ... partialize: (state) => Object.fromEntries( Object.entries(state).filter(([key]) => !['foo'].includes(key)), ), }, ), ) ``` Or you could allow only specific fields using the following: ```ts export const useBoundStore = create( persist( (set, get) => ({ foo: 0, bar: 1, }), { // ... partialize: (state) => ({ foo: state.foo }), }, ), ) ``` ### `onRehydrateStorage` > Type: `(state: Object) => ((state?: Object, error?: Error) => void) | void` This option enables you to pass a listener function that will be called when the storage is hydrated. Example: ```ts export const useBoundStore = create( persist( (set, get) => ({ // ... }), { // ... onRehydrateStorage: (state) => { console.log('hydration starts') // optional return (state, error) => { if (error) { console.log('an error happened during hydration', error) } else { console.log('hydration finished') } } }, }, ), ) ``` ### `version` > Type: `number` > Default: `0` If you want to introduce a breaking change in your storage (e.g. renaming a field), you can specify a new version number. By default, if the version in the storage does not match the version in the code, the stored value won't be used. You can use the [migrate](#migrate) function (see below) to handle breaking changes in order to persist previously stored data. ### `migrate` > Type: `(persistedState: Object, version: number) => Object | Promise` > Default: `(persistedState) => persistedState` You can use this option to handle versions migration. The migrate function takes the persisted state and the version number as arguments. It must return a state that is compliant to the latest version (the version in the code). For instance, if you want to rename a field, you can use the following: ```ts export const useBoundStore = create( persist( (set, get) => ({ newField: 0, // let's say this field was named otherwise in version 0 }), { // ... version: 1, // a migration will be triggered if the version in the storage mismatches this one migrate: (persistedState, version) => { if (version === 0) { // if the stored value is in version 0, we rename the field to the new name persistedState.newField = persistedState.oldField delete persistedState.oldField } return persistedState }, }, ), ) ``` ### `merge` > Type: `(persistedState: Object, currentState: Object) => Object` > Default: `(persistedState, currentState) => ({ ...currentState, ...persistedState })` In some cases, you might want to use a custom merge function to merge the persisted value with the current state. By default, the middleware does a shallow merge. The shallow merge might not be enough if you have partially persisted nested objects. For instance, if the storage contains the following: ```ts { foo: { bar: 0, } } ``` But your Zustand store contains: ```ts { foo: { bar: 0, baz: 1, } } ``` The shallow merge will erase the `baz` field from the `foo` object. One way to fix this would be to give a custom deep merge function: ```ts export const useBoundStore = create( persist( (set, get) => ({ foo: { bar: 0, baz: 1, }, }), { // ... merge: (persistedState, currentState) => deepMerge(currentState, persistedState), }, ), ) ``` ### `skipHydration` > Type: `boolean | undefined` > Default: `undefined` By default the store will be hydrated on initialization. In some applications you may need to control when the first hydration occurs. For example, in server-rendered apps. If you set `skipHydration`, the initial call for hydration isn't called, and it is left up to you to manually call `rehydrate()`. ```ts export const useBoundStore = create( persist( () => ({ count: 0, // ... }), { // ... skipHydration: true, }, ), ) ``` ```tsx import { useBoundStore } from './path-to-store'; export function StoreConsumer() { // hydrate persisted store after on mount useEffect(() => { useBoundStore.persist.rehydrate(); }, []) return ( //... ) } ``` ## API > Version: >=3.6.3 The Persist API enables you to do a number of interactions with the Persist middleware from inside or outside of a React component. ### `getOptions` > Type: `() => Partial` > Returns: Options of the Persist middleware For example, it can be used to obtain the storage name: ```ts useBoundStore.persist.getOptions().name ``` ### `setOptions` > Type: `(newOptions: Partial) => void` Changes the middleware options. Note that the new options will be merged with the current ones. For instance, this can be used to change the storage name: ```ts useBoundStore.persist.setOptions({ name: 'new-name', }) ``` Or even to change the storage engine: ```ts useBoundStore.persist.setOptions({ storage: createJSONStorage(() => sessionStorage), }) ``` ### `clearStorage` > Type: `() => void` Clears everything stored under the [name](#name) key. ```ts useBoundStore.persist.clearStorage() ``` ### `rehydrate` > Type: `() => Promise` In some cases, you might want to trigger the rehydration manually. This can be done by calling the `rehydrate` method. ```ts await useBoundStore.persist.rehydrate() ``` ### `hasHydrated` > Type: `() => boolean` This is a non-reactive getter to check if the storage has been hydrated (note that it updates when calling [`rehydrate`](#rehydrate)). ```ts useBoundStore.persist.hasHydrated() ``` ### `onHydrate` > Type: `(listener: (state) => void) => () => void` > Returns: Unsubscribe function This listener will be called when the hydration process starts. ```ts const unsub = useBoundStore.persist.onHydrate((state) => { console.log('hydration starts') }) // later on... unsub() ``` ### `onFinishHydration` > Type: `(listener: (state) => void) => () => void` > Returns: Unsubscribe function This listener will be called when the hydration process ends. ```ts const unsub = useBoundStore.persist.onFinishHydration((state) => { console.log('hydration finished') }) // later on... unsub() ``` ### `createJSONStorage` > Type: `(getStorage: () => StateStorage, options?: JsonStorageOptions) => StateStorage` > Returns: `PersistStorage` This helper function enables you to create a [`storage`](#storage) object which is useful when you want to use a custom storage engine. `getStorage` is a function that returns the storage engine with the properties `getItem`, `setItem`, and `removeItem`. `options` is an optional object that can be used to customize the serialization and deserialization of the data. `options.reviver` is a function that is passed to `JSON.parse` to deserialize the data. `options.replacer` is a function that is passed to `JSON.stringify` to serialize the data. ```ts import { createJSONStorage } from 'zustand/middleware' const storage = createJSONStorage(() => sessionStorage, { reviver: (key, value) => { if (value && value.type === 'date') { return new Date(value) } return value }, replacer: (key, value) => { // NOTE: the result of `.toJSON()` is passed to the // replacer function as value if is available so // a Date is always a `string` at this point if (key === 'someDate') return { type: 'date', value } return value }, }) ``` ## Hydration and asynchronous storages To explain what is the "cost" of asynchronous storages, you need to understand what is hydration. In a nutshell, hydration is a process of retrieving persisted state from the storage and merging it with the current state. The Persist middleware does two kinds of hydration: synchronous and asynchronous. If the given storage is synchronous (e.g., `localStorage`), hydration will be done synchronously. On the other hand, if the given storage is asynchronous (e.g., `AsyncStorage`), hydration will be done asynchronously (shocking, I know!). But what's the catch? With synchronous hydration, the Zustand store will already have been hydrated at its creation. In contrast, with asynchronous hydration, the Zustand store will be hydrated later on, in a microtask. Why does it matter? Asynchronous hydration can cause some unexpected behaviors. For instance, if you use Zustand in a React app, the store will **not** be hydrated at the initial render. In cases where your app depends on the persisted value at page load, you might want to wait until the store has been hydrated before showing anything. For example, your app might think the user is not logged in because it's the default, but in reality the store has not been hydrated yet. If your app does depends on the persisted state at page load, see [_How can I check if my store has been hydrated_](#how-can-i-check-if-my-store-has-been-hydrated) in the [FAQ](#faq) section below. ### Usage in Next.js NextJS uses Server Side Rendering, and it will compare the rendered component on the server with the one rendered on client. But since you are using data from browser to change your component, the two renders will differ and Next will throw a warning at you. The errors usually are: - Text content does not match server-rendered HTML - Hydration failed because the initial UI does not match what was rendered on the server - There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering To solve these errors, create a custom hook so that Zustand waits a little before changing your components. Create a file with the following: ```ts // useStore.ts import { useState, useEffect } from 'react' const useStore = ( store: (callback: (state: T) => unknown) => unknown, callback: (state: T) => F, ) => { const result = store(callback) as F const [data, setData] = useState() useEffect(() => { setData(result) }, [result]) return data } export default useStore ``` Now in your pages, you will use the hook a little bit differently: ```ts // useBearStore.ts import { create } from 'zustand' import { persist } from 'zustand/middleware' // the store itself does not need any change export const useBearStore = create( persist( (set, get) => ({ bears: 0, addABear: () => set({ bears: get().bears + 1 }), }), { name: 'food-storage', }, ), ) ``` ```ts // yourComponent.tsx import useStore from './useStore' import { useBearStore } from './stores/useBearStore' const bears = useStore(useBearStore, (state) => state.bears) ``` Credits: [This reply to an issue](https://github.com/pmndrs/zustand/issues/938#issuecomment-1481801942), which points to [this blog post](https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5). ## FAQ ### How can I check if my store has been hydrated There are a few different ways to do this. You can use the [`onRehydrateStorage`](#onrehydratestorage) listener function to update a field in the store: ```ts const useBoundStore = create( persist( (set, get) => ({ // ... _hasHydrated: false, setHasHydrated: (state) => { set({ _hasHydrated: state }); } }), { // ... onRehydrateStorage: (state) => { return () => state.setHasHydrated(true) } } ) ); export default function App() { const hasHydrated = useBoundStore(state => state._hasHydrated); if (!hasHydrated) { return

Loading...

} return ( // ... ); } ``` You can also create a custom `useHydration` hook: ```ts const useBoundStore = create(persist(...)) const useHydration = () => { const [hydrated, setHydrated] = useState(false) useEffect(() => { // Note: This is just in case you want to take into account manual rehydration. // You can remove the following line if you don't need it. const unsubHydrate = useBoundStore.persist.onHydrate(() => setHydrated(false)) const unsubFinishHydration = useBoundStore.persist.onFinishHydration(() => setHydrated(true)) setHydrated(useBoundStore.persist.hasHydrated()) return () => { unsubHydrate() unsubFinishHydration() } }, []) return hydrated } ``` ### How can I use a custom storage engine If the storage you want to use does not match the expected API, you can create your own storage: ```ts import { create } from 'zustand' import { persist, createJSONStorage, StateStorage } from 'zustand/middleware' import { get, set, del } from 'idb-keyval' // can use anything: IndexedDB, Ionic Storage, etc. // Custom storage object const storage: StateStorage = { getItem: async (name: string): Promise => { console.log(name, 'has been retrieved') return (await get(name)) || null }, setItem: async (name: string, value: string): Promise => { console.log(name, 'with value', value, 'has been saved') await set(name, value) }, removeItem: async (name: string): Promise => { console.log(name, 'has been deleted') await del(name) }, } export const useBoundStore = create( persist( (set, get) => ({ bears: 0, addABear: () => set({ bears: get().bears + 1 }), }), { name: 'food-storage', // unique name storage: createJSONStorage(() => storage), }, ), ) ``` If you're using a type that `JSON.stringify()` doesn't support, you'll need to write your own serialization/deserialization code. However, if this is tedious, you can use third-party libraries to serialize and deserialize different types of data. For example, [Superjson](https://github.com/blitz-js/superjson) can serialize data along with its type, allowing the data to be parsed back to its original type upon deserialization ```ts import superjson from 'superjson' // can use anything: serialize-javascript, devalue, etc. import { PersistStorage } from 'zustand/middleware' interface BearState { bear: Map fish: Set time: Date query: RegExp } const storage: PersistStorage = { getItem: (name) => { const str = localStorage.getItem(name) if (!str) return null return superjson.parse(str) }, setItem: (name, value) => { localStorage.setItem(name, superjson.stringify(value)) }, removeItem: (name) => localStorage.removeItem(name), } const initialState: BearState = { bear: new Map(), fish: new Set(), time: new Date(), query: new RegExp(''), } export const useBearStore = create()( persist( (set) => ({ ...initialState, // ... }), { name: 'food-storage', storage, }, ), ) ``` ### How can I rehydrate on storage event You can use the Persist API to create your own implementation, similar to the example below: ```ts type StoreWithPersist = Mutate, [["zustand/persist", unknown]]> export const withStorageDOMEvents = (store: StoreWithPersist) => { const storageEventCallback = (e: StorageEvent) => { if (e.key === store.persist.getOptions().name && e.newValue) { store.persist.rehydrate() } } window.addEventListener('storage', storageEventCallback) return () => { window.removeEventListener('storage', storageEventCallback) } } const useBoundStore = create(persist(...)) withStorageDOMEvents(useBoundStore) ``` ### How do I use it with TypeScript Basic typescript usage doesn't require anything special except for writing `create()(...)` instead of `create(...)`. ```tsx import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' interface MyState { bears: number addABear: () => void } export const useBearStore = create()( persist( (set, get) => ({ bears: 0, addABear: () => set({ bears: get().bears + 1 }), }), { name: 'food-storage', // name of item in the storage (must be unique) storage: createJSONStorage(() => sessionStorage), // (optional) by default the 'localStorage' is used partialize: (state) => ({ bears: state.bears }), }, ), ) ``` ### How do I use it with Map and Set In order to persist object types such as `Map` and `Set`, they will need to be converted to JSON-serializable types such as an `Array` which can be done by defining a custom `storage` engine. Let's say your state uses `Map` to handle a list of `transactions`, then you can convert the `Map` into an `Array` in the `storage` prop which is shown below: ```ts interface BearState { . . . transactions: Map } storage: { getItem: (name) => { const str = localStorage.getItem(name); if (!str) return null; const existingValue = JSON.parse(str); return { ...existingValue, state: { ...existingValue.state, transactions: new Map(existingValue.state.transactions), } } }, setItem: (name, newValue: StorageValue) => { // functions cannot be JSON encoded const str = JSON.stringify({ ...newValue, state: { ...newValue.state, transactions: Array.from(newValue.state.transactions.entries()), }, }) localStorage.setItem(name, str) }, removeItem: (name) => localStorage.removeItem(name), }, ``` ================================================ FILE: docs/reference/integrations/third-party-libraries.md ================================================ --- title: Third-party Libraries nav: 36 --- Zustand provides bear necessities for state management. Although it is great for most projects, some users wish to extend the library's feature set. This can be done using third-party libraries created by the community. > Disclaimer: These libraries may have bugs, limited maintenance, > or other limitations, and are not officially recommended > by pmndrs or the Zustand maintainers. > This list aims to provide a good starting point > for someone looking to extend Zustand's feature set. - [@colorfy-software/zfy](https://colorfy-software.gitbook.io/zfy/) — 🧸 Useful helpers for state management in React with Zustand. - [@csark0812/zustand-expo-devtools](https://github.com/csark0812/zustand-expo-devtools) — 🧭 Connect Zustand to Redux DevTools in Expo + React Native using the official Expo DevTools plugin system. - [@csark0812/zustand-getters](https://github.com/csark0812/zustand-getters) — 🔄 Make JavaScript object getters reactive in Zustand stores — define derived values with `get propertyName()` and they automatically trigger subscription updates when accessed. - [@davstack/store](https://www.npmjs.com/package/@davstack/store) — A zustand store factory that auto generates selectors with get/set/use methods, supports inferred types, and makes global / local state management easy. - [@dhmk/zustand-lens](https://github.com/dhmk083/dhmk-zustand-lens) — Lens support for Zustand. - [@hpkv/zustand-multiplayer](https://github.com/hpkv-io/zustand-multiplayer/tree/main/packages/zustand-multiplayer) — HPKV multiplayer middleware for building realtime collaborative applications - [@liveblocks/zustand](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks-zustand) — Liveblocks middleware to make your application multiplayer. - [@prncss-xyz/zustand-optics](https://github.com/prncss-xyz/zustand-optics) — An adapter for [optics-ts](https://github.com/akheron/optics-ts). - [auto-zustand-selectors-hook](https://github.com/Albert-Gao/auto-zustand-selectors-hook) — Automatic generation of Zustand hooks with Typescript support. - [derive-zustand](https://github.com/zustandjs/derive-zustand) — A function to create a derived Zustand store from other Zustand stores. - [geschichte](https://github.com/BowlingX/geschichte) — Zustand and Immer-based hook to manage query parameters. - [leiten-zustand](https://github.com/hecmatyar/leiten-zustand) — Cleans your store from boilerplate for requests and data transformation. - [leo-query](https://github.com/steaks/leo-query) — A simple library to connect async queries to Zustand stores. - [mobz](https://github.com/2A5F/Mobz) — Zustand-style MobX API. - [ngx-zustand](https://github.com/JoaoPauloLousada/ngx-zustand) — A Zustand adapter for Angular. - [persist-and-sync](https://github.com/mayank1513/persist-and-sync) — Zustand middleware to easily persist and sync Zustand state between tabs/windows/iframes with same origin. - [shared-zustand](https://github.com/Tom-Julux/shared-zustand) — Cross-tab state sharing for Zustand. - [simple-zustand-devtools](https://github.com/beerose/simple-zustand-devtools) — 🐻⚛️ Inspect your Zustand store in React DevTools. - [solid-zustand](https://github.com/wobsoriano/solid-zustand) — State management in Solid using Zustand. - [treeshakable](https://github.com/react18-tools/treeshakable) — A wrapper for library creators to avoid redundant store creation. - [use-broadcast-ts](https://github.com/Romainlg29/use-broadcast) — Zustand middleware to share state between tabs. - [use-post-message-ts](https://github.com/paulschoen/use-post-message) — Zustand middleware for sharing state between cross-origin iframes via postMessage browser method. - [use-zustand](https://github.com/zustandjs/use-zustand) — Another custom hook to use Zustand vanilla store. - [vue-zustand](https://github.com/wobsoriano/vue-zustand) — State management solution for Vue based on Zustand. - [zoov](https://github.com/InfiniteXyy/zoov) — State management solution based on Zustand with Module-like API. - [zubridge](https://github.com/goosewobbler/zubridge) — Use Zustand in cross-platform apps, seamlessly. Supports Electron & Tauri. - [zukeeper](https://github.com/oslabs-beta/Zukeeper) — Native devtools with state and action tracking, diffing, tree display, and time travel - [zundo](https://github.com/charkour/zundo) — 🍜 Undo and redo middleware for Zustand, enabling time-travel in your apps. - [zustand-ards](https://github.com/ivoilic/zustand-ards) — 💁 Simple opinionated utilities for example alternative selector formats and default shallow hooks - [zustand-async-slice](https://github.com/mym0404/zustand-async-slice) — Simple Zustand utility to create Async Slice. TypeScript Fully Supported 🖖 - [zustand-boilerplate](https://github.com/sagiereder/zustand-boilerplate) — A tool that automatically generates getters, setters and more for your zustand store. - [zustand-computed](https://github.com/chrisvander/zustand-computed) — A Zustand middleware to create computed states. - [zustand-computed-state](https://github.com/yasintz/zustand-computed-state) — Simple middleware to add computed states. - [zustand-constate](https://github.com/ntvinhit/zustand-constate) — Context-based state management based on Zustand and taking ideas from Constate. - [zustand-context](https://github.com/fredericoo/zustand-context) — Create a zustand store in React Context, containing an initial value, or use it in your components with isolated, mockable instances. - [zustand-create-setter-fn](https://www.npmjs.com/package/zustand-create-setter-fn) — A fully type safe utility for Zustand that allows you to easily update state using React style `setState` functions (framework agnostic, doesn't require React). - [zustand-di](https://github.com/charkour/zustand-di) — use react props to init zustand stores - [zustand-forms](https://github.com/Conduct/zustand-forms) — Fast, type safe form states as Zustand stores. - [zustand-hash-storage](https://github.com/MartinGamesCZ/zustand-hash-storage) — Zustand middleware for saving state into URL hash, b64 encoded (can be configured) and debounce timer. - [zustand-injectors](https://github.com/zustandjs/zustand-injectors) — A sweet way to lazy load slices - [zustand-interval-persist](https://www.npmjs.com/package/zustand-interval-persist) — An enhancement for zustand that enables automatic saving of the store's state to the specified storage at regular interval. - [zustand-lit](https://github.com/ennjin/zustand-lit) — A zustand adapter for lit.js (LitElement) - [zustand-middleware-computed-state](https://github.com/cmlarsen/zustand-middleware-computed-state) — A dead simple middleware for adding computed state to Zustand. - [zustand-middleware-xstate](https://github.com/biowaffeln/zustand-middleware-xstate) — A middleware for putting XState state machines into a global Zustand store. - [zustand-middleware-yjs](https://github.com/joebobmiles/zustand-middleware-yjs) — A middleware for synchronizing Zustand stores with Yjs. - [zustand-mmkv-storage](https://github.com/1mehdifaraji/zustand-mmkv-storage) — Fast, lightweight MMKV storage adapter for Zustand persist middleware in React Native. - [zustand-multi-persist](https://github.com/mooalot/zustand-multi-persist) — A middleware for persisting and rehydrating state to multiple storage engines. - [zustand-mutable](https://github.com/zustandjs/zustand-mutable) — A sweet way to use immer-like mutable updates. - [zustand-namespaces](https://github.com/mooalot/zustand-namespaces) — One store to rule them all. Namespaced Zustand stores. - [zustand-persist](https://github.com/roadmanfong/zustand-persist) — A middleware for persisting and rehydrating state. - [zustand-pub](https://github.com/AwesomeDevin/zustand-pub) — Cross-Application/Cross-Framework State Management And Sharing based on zustand and zustand-vue for React/Vue. - [zustand-querystring](https://github.com/nitedani/zustand-querystring) — A Zustand middleware that syncs the store with the querystring. - [zustand-rx](https://github.com/patdx/zustand-rx) — A Zustand middleware enabling you to subscribe to a store as an RxJS Observable. - [zustand-saga](https://github.com/Nowsta/zustand-saga) — A Zustand middleware for redux-saga (minus redux). - [zustand-slices](https://github.com/zustandjs/zustand-slices) — A slice utility for Zustand. - [zustand-store-addons](https://github.com/Diablow/zustand-store-addons) — React state management addons for Zustand. - [zustand-sync-tabs](https://github.com/mayank1513/zustand-sync-tabs) — Zustand middleware to easily sync Zustand state between tabs/windows/iframes with same origin. - [zustand-utils](https://www.npmjs.com/package/zustand-utils) — Utilities for Zustand — a `createContext` replacement, a devtools wrapper, and a store-updater factory function. - [zustand-valtio](https://github.com/zustandjs/zustand-valtio) — A sweet combination of Zustand and Valtio - [zustand-vue](https://github.com/AwesomeDevin/zustand-vue) — State management for vue (Vue3 / Vue2) based on zustand. - [zustand-x](https://github.com/udecode/zustand-x) — Zustand store factory for a best-in-class developer experience. - [zustand-xs](https://github.com/zustandjs/zustand-xs) — XState/store compabile middleware for Zustand - [zustand-yjs](https://github.com/tandem-pt/zustand-yjs) — Zustand stores for Yjs structures. - [zusteller](https://github.com/timkindberg/zusteller) — Your global state savior. "Just hooks" + Zustand. - [zustorm](https://github.com/mooalot/zustorm) — A simple and powerful form library for Zustand. - [zusty](https://github.com/oslabs-beta/Zusty) — Zustand tool to assist debugging with time travel, action logs, state snapshots, store view, render time metrics and state component tree. ================================================ FILE: docs/reference/middlewares/combine.md ================================================ --- title: combine description: How to create a store and get types automatically inferred nav: 32 --- # combine `combine` middleware lets you create a cohesive state by merging an initial state with a state creator function that adds new state slices and actions. This is really helpful as it automatically infers types, so there’s no need for explicit type definitions. > [!TIP] > This makes state management more straightforward and efficient by making curried version of > `create` and `createStore` not necessary for middleware usage. ```js const nextStateCreatorFn = combine(initialState, additionalStateCreatorFn) ``` - [Types](#types) - [Signature](#signature) - [Reference](#reference) - [Usage](#usage) - [Creating a store with inferred types](#creating-a-store-with-inferred-types) - [Troubleshooting](#troubleshooting) ## Types ### Signature ```ts combine(initialState: T, additionalStateCreatorFn: StateCreator): StateCreator & U, [], []> ``` ## Reference ### `combine(initialState, additionalStateCreatorFn)` #### Parameters - `initialState`: The value you want the state to be initially. It can be a value of any type, except a function. - `additionalStateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. #### Returns `combine` returns a state creator function. ## Usage ### Creating a store with inferred types This example shows you how you can create a store and get types automatically inferred, so you don’t need to define them explicitly. ```ts import { createStore } from 'zustand/vanilla' import { combine } from 'zustand/middleware' const positionStore = createStore( combine({ position: { x: 0, y: 0 } }, (set) => ({ setPosition: (position) => set({ position }), })), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ## Troubleshooting TBD ================================================ FILE: docs/reference/middlewares/devtools.md ================================================ --- title: devtools description: How to time-travel debug your store nav: 29 --- # devtools `devtools` middleware lets you use [Redux DevTools Extension](https://github.com/reduxjs/redux-devtools) 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). > [!IMPORTANT] > In order to use `devtools` from `zustand/middleware` you need to install > `@redux-devtools/extension` library. ```js const nextStateCreatorFn = devtools(stateCreatorFn, devtoolsOptions) ``` - [Types](#types) - [Signature](#signature) - [Mutator](#mutator) - [Reference](#reference) - [Usage](#usage) - [Debugging a store](#debugging-a-store) - [Debugging a Slices pattern based store](#debugging-a-slices-pattern-based-store) - [Filtering actions with actionsDenylist](#filtering-actions-with-actionsdenylist) - [Cleanup](#cleanup) - [Troubleshooting](#troubleshooting) - [Only one store is displayed](#only-one-store-is-displayed) - [Action names are labeled as 'anonymous'](#all-action-names-are-labeled-as-'anonymous') ## Types ### Signature ```ts devtools(stateCreatorFn: StateCreator, devtoolsOptions?: DevtoolsOptions): StateCreator ``` ### Mutator ```ts ;['zustand/devtools', never] ``` ## Reference ### `devtools(stateCreatorFn, devtoolsOptions)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. - **optional** `devtoolsOptions`: An object to define `Redux Devtools` options. - **optional** `name`: A custom identifier for the connection in the Redux DevTools. - **optional** `enabled`: Defaults to `true` when is on development mode, and defaults to `false` when is on production mode. Enables or disables the Redux DevTools integration for this store. - **optional** `anonymousActionType`: Defaults to the inferred action type or `anonymous` if unavailable. A string to use as the action type for anonymous mutations in the Redux DevTools. - **optional** `store`: A custom identifier for the store in the Redux DevTools. - **optional** `actionsDenylist`: A string or array of strings (regex patterns) that specify which actions should be filtered out from Redux DevTools. This option is passed directly to Redux DevTools for filtering. For example, `['secret.*']` will filter out all actions starting with "secret". #### Returns `devtools` returns a state creator function. ## Usage ### Debugging a store This example shows you how you can use `Redux Devtools` to debug a store ```ts import { create, StateCreator } from 'zustand' import { devtools } from 'zustand/middleware' type JungleStore = { bears: number addBear: () => void fishes: number addFish: () => void } const useJungleStore = create()( devtools((set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 }), undefined, 'jungle/addBear'), fishes: 0, addFish: () => set( (state) => ({ fishes: state.fishes + 1 }), undefined, 'jungle/addFish', ), })), ) ``` ### Debugging a Slices pattern based store This example shows you how you can use `Redux Devtools` to debug a Slices pattern based store ```ts import { create, StateCreator } from 'zustand' import { devtools } from 'zustand/middleware' type BearSlice = { bears: number addBear: () => void } type FishSlice = { fishes: number addFish: () => void } type JungleStore = BearSlice & FishSlice const createBearSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], BearSlice > = (set) => ({ bears: 0, addBear: () => set( (state) => ({ bears: state.bears + 1 }), undefined, 'jungle:bear/addBear', ), }) const createFishSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], FishSlice > = (set) => ({ fishes: 0, addFish: () => set( (state) => ({ fishes: state.fishes + 1 }), undefined, 'jungle:fish/addFish', ), }) const useJungleStore = create()( devtools((...args) => ({ ...createBearSlice(...args), ...createFishSlice(...args), })), ) ``` ### Filtering actions with actionsDenylist You can filter out specific actions from Redux DevTools using the `actionsDenylist` option. This is useful for hiding internal or sensitive actions from the DevTools timeline. ```ts import { create } from 'zustand' import { devtools } from 'zustand/middleware' type Store = { user: string | null token: string | null login: (user: string, token: string) => void logout: () => void updateData: () => void } const useStore = create()( devtools( (set) => ({ user: null, token: null, login: (user, token) => set({ user, token }, undefined, 'auth/login'), logout: () => set({ user: null, token: null }, undefined, 'auth/logout'), updateData: () => set({ user: 'updated' }, undefined, 'internal/updateData'), }), { name: 'AuthStore', // Filter out actions matching these regex patterns actionsDenylist: ['internal/.*'], // Hides all 'internal/*' actions }, ), ) ``` You can also use a single regex string: ```ts const useStore = create()( devtools( (set) => ({ // ... state and actions }), { name: 'MyStore', actionsDenylist: 'secret.*', // Hides all actions starting with 'secret' }, ), ) ``` > [!NOTE] > The `actionsDenylist` option uses regex pattern matching and is handled directly by Redux DevTools Extension. > All actions are still sent to DevTools, but matching actions are filtered from the display. ### Cleanup When a store is no longer needed, you can clean up the Redux DevTools connection by calling the `cleanup` method on the store: ```ts import { create } from 'zustand' import { devtools } from 'zustand/middleware' const useStore = create( devtools((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })), ) // When you're done with the store, clean it up useStore.devtools.cleanup() ``` This is particularly useful in applications that wrap store in context or create multiple stores dynamically. ## Troubleshooting ### Only one store is displayed By default, `Redux Devtools` only show one store at a time, so in order to see other stores you need to use store selector and choose a different store. ### All action names are labeled as 'anonymous' If an action type name is not provided, it is defaulted to "anonymous". You can customize this default value by providing a `anonymousActionType` parameter: For instance the next example doesn't have action type name: ```ts import { create, StateCreator } from 'zustand' import { devtools } from 'zustand/middleware' type BearSlice = { bears: number addBear: () => void } type FishSlice = { fishes: number addFish: () => void } type JungleStore = BearSlice & FishSlice const createBearSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], BearSlice > = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), }) const createFishSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], FishSlice > = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) const useJungleStore = create()( devtools((...args) => ({ ...createBearSlice(...args), ...createFishSlice(...args), })), ) ``` In order to fix the previous example, we need to provide an action type name as the third parameter. Additionally, to preserve the default behavior of the replacement logic, the second parameter should be set to `undefined`. Here's the fixed previous example ```ts import { create, StateCreator } from 'zustand' type BearSlice = { bears: number addBear: () => void } type FishSlice = { fishes: number addFish: () => void } type JungleStore = BearSlice & FishSlice const createBearSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], BearSlice > = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 }), undefined, 'bear/addBear'), }) const createFishSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], FishSlice > = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 }), undefined, 'fish/addFish'), }) const useJungleStore = create()( devtools((...args) => ({ ...createBearSlice(...args), ...createFishSlice(...args), })), ) ``` > [!IMPORTANT] > Do not set the second parameter to `true` or `false` unless you want to override the default > replacement logic ================================================ FILE: docs/reference/middlewares/immer.md ================================================ --- title: immer description: How to perform immutable updates in a store without boilerplate code nav: 31 --- # immer `immer` middleware lets you perform immutable updates. > [!IMPORTANT] > In order to use `immer` from `zustand/middleware/immer` you need to install > `immer` library. ```js const nextStateCreatorFn = immer(stateCreatorFn) ``` - [Types](#types) - [Signature](#signature) - [Mutator](#mutator) - [Reference](#reference) - [Usage](#usage) - [Troubleshooting](#troubleshooting) ## Types ### Signature ```ts immer(stateCreatorFn: StateCreator): StateCreator ``` ### Mutator ```ts ;['zustand/immer', never] ``` ## Reference ### `immer(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. #### Returns `immer` returns a state creator function. ## Usage ### Updating state without boilerplate code In the next example, we're going to update the `person` object. Since it's a nested object, we need to create a copy of the entire object before making the update. ```ts import { createStore } from 'zustand/vanilla' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: ( nextPerson: ( person: PersonStoreState['person'], ) => PersonStoreState['person'] | PersonStoreState['person'], ) => void } type PersonStore = PersonStoreState & PersonStoreActions const personStore = createStore()((set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (nextPerson) => set((state) => ({ person: typeof nextPerson === 'function' ? nextPerson(state.person) : nextPerson, })), })) const $firstNameInput = document.getElementById( 'first-name', ) as HTMLInputElement const $lastNameInput = document.getElementById('last-name') as HTMLInputElement const $emailInput = document.getElementById('email') as HTMLInputElement const $result = document.getElementById('result') as HTMLDivElement function handleFirstNameChange(event: Event) { personStore.getState().setPerson((person) => ({ ...person, firstName: (event.target as any).value, })) } function handleLastNameChange(event: Event) { personStore.getState().setPerson((person) => ({ ...person, lastName: (event.target as any).value, })) } function handleEmailChange(event: Event) { personStore.getState().setPerson((person) => ({ ...person, email: (event.target as any).value, })) } $firstNameInput.addEventListener('input', handleFirstNameChange) $lastNameInput.addEventListener('input', handleLastNameChange) $emailInput.addEventListener('input', handleEmailChange) const render: Parameters[0] = (state) => { $firstNameInput.value = state.person.firstName $lastNameInput.value = state.person.lastName $emailInput.value = state.person.email $result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})` } render(personStore.getInitialState(), personStore.getInitialState()) personStore.subscribe(render) ``` Here's the `html` code ```html

``` To avoid manually copying the entire object before making updates, we'll use the `immer` middleware. ```ts import { createStore } from 'zustand/vanilla' import { immer } from 'zustand/middleware/immer' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: ( nextPerson: ( person: PersonStoreState['person'], ) => PersonStoreState['person'] | PersonStoreState['person'], ) => void } type PersonStore = PersonStoreState & PersonStoreActions const personStore = createStore()( immer((set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (nextPerson) => set((state) => { state.person = typeof nextPerson === 'function' ? nextPerson(state.person) : nextPerson }), })), ) const $firstNameInput = document.getElementById( 'first-name', ) as HTMLInputElement const $lastNameInput = document.getElementById('last-name') as HTMLInputElement const $emailInput = document.getElementById('email') as HTMLInputElement const $result = document.getElementById('result') as HTMLDivElement function handleFirstNameChange(event: Event) { personStore.getState().setPerson((person) => { person.firstName = (event.target as any).value }) } function handleLastNameChange(event: Event) { personStore.getState().setPerson((person) => { person.lastName = (event.target as any).value }) } function handleEmailChange(event: Event) { personStore.getState().setPerson((person) => { person.email = (event.target as any).value }) } $firstNameInput.addEventListener('input', handleFirstNameChange) $lastNameInput.addEventListener('input', handleLastNameChange) $emailInput.addEventListener('input', handleEmailChange) const render: Parameters[0] = (state) => { $firstNameInput.value = state.person.firstName $lastNameInput.value = state.person.lastName $emailInput.value = state.person.email $result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})` } render(personStore.getInitialState(), personStore.getInitialState()) personStore.subscribe(render) ``` ## Troubleshooting TBD ================================================ FILE: docs/reference/middlewares/persist.md ================================================ --- title: persist description: How to persist a store nav: 28 --- # persist `persist` middleware lets you persist a store's state across page reloads or application restarts. ```js const nextStateCreatorFn = persist(stateCreatorFn, persistOptions) ``` - [Types](#types) - [Signature](#signature) - [Mutator](#mutator) - [Reference](#reference) - [Usage](#usage) - [Persisting a state](#persisting-a-state) - [Persisting a state partially](#persisting-a-state-partially) - [Persisting a state with custom storage](#persisting-a-state-with-custom-storage) - [Persisting a state through versioning and migrations](#persisting-a-state-through-versioning-and-migrations) - [Persisting a state with nested objects](#persisting-a-state-with-nested-objects) - [Persisting a state and hydrate it manually](#persisting-a-state-and-hydrate-it-manually) - [Troubleshooting](#troubleshooting) ## Types ### Signature ```ts persist(stateCreatorFn: StateCreator, persistOptions?: PersistOptions): StateCreator ``` ### Mutator ```ts ;['zustand/persist', U] ``` ## Reference ### `persist(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. - `persistOptions`: An object to define storage options. - `name`: A unique name of the item for your store in the storage. - **optional** `storage`: Defaults to `createJSONStorage(() => localStorage)`. - **optional** `partialize`: A function to filter state fields before persisting it. - **optional** `onRehydrateStorage`: A function or function returning a function that allows custom logic before and after state rehydration. - **optional** `version`: A version number for the persisted state. If the stored state version doesn't match, it won't be used. - **optional** `migrate`: A function to migrate persisted state if the version mismatch occurs. - **optional** `merge`: A function for custom logic when merging persisted state with the current state during rehydration. Defaults to a shallow merge. - **optional** `skipHydration`: Defaults to `false`. If `true`, the middleware won't automatically rehydrate the state on initialization. Use `rehydrate` function manually in this case. This is useful for server-side rendering (SSR) applications. #### Returns `persist` returns a state creator function. ## Usage ### Persisting a state In this tutorial, we'll create a simple position tracker using vanilla store and the `persist` middleware. The example tracks the `position` of the mouse as it moves within a container and stores the `position` in local storage, so it persists even when the page reloads. We start by setting up a vanilla store that holds the position (an object with `x` and `y` coordinates) and an action to update it. We'll also use the `persist` middleware to store the position in `localStorage`. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage' }, ), ) ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the complete code. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage' }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Persisting a state partially In this tutorial, we'll create a simple position tracker using vanilla store and the `persist` middleware. Additionally, we'll show you how to persist only part of the state (partial persistence), which can be useful when you don’t want to store the entire state in `localStorage`. We’ll first create a vanilla store that holds the position state and actions to update it. We'll use the `persist` middleware to persist only the relevant part of the state (in this case, the context containing the position). ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { context: { position: { x: number; y: number } } } type PositionStoreActions = { actions: { setPosition: ( nextPosition: PositionStoreState['context']['position'], ) => void } } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ context: { position: { x: 0, y: 0 }, }, actions: { setPosition: (position) => set({ context: { position } }), }, }), { name: 'position-storage', partialize: (state) => ({ context: state.context }), }, ), ) ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().actions.setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.context.position.x}px, ${state.context.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the full code to create a dot that follows your mouse movement inside a container and persists the `context` in `localStorage`. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { context: { position: { x: number; y: number } } } type PositionStoreActions = { actions: { setPosition: ( nextPosition: PositionStoreState['context']['position'], ) => void } } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ context: { position: { x: 0, y: 0 }, }, actions: { setPosition: (position) => set({ context: { position } }), }, }), { name: 'position-storage', partialize: (state) => ({ context: state.context }), }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().actions.setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.context.position.x}px, ${state.context.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Persisting a state with custom storage In this mini tutorial, we’ll create a simple position-tracking system using vanilla store, where the position state is persisted in the URL's search parameters. This approach allows state persistence directly in the browser's URL, which can be useful for maintaining state across page reloads or sharing links with state embedded. We need to implement functions to manipulate URL search parameters as if they were a storage mechanism. This includes retrieving, setting, and removing parameters. ```ts const getSearchParams = () => { return new URL(location.href).searchParams } const updateSearchParams = (searchParams: URLSearchParams) => { window.history.replaceState( {}, '', `${location.pathname}?${searchParams.toString()}`, ) } const getSearchParam = (key: string) => { const searchParams = getSearchParams() return searchParams.get(key) } const updateSearchParam = (key: string, value: string) => { const searchParams = getSearchParams() searchParams.set(key, value) updateSearchParams(searchParams) } const removeSearchParam = (key: string) => { const searchParams = getSearchParams() searchParams.delete(key) updateSearchParams(searchParams) } ``` To use the URL search parameters as storage, we define a `searchParamsStorage` object with `getItem`, `setItem`, and `removeItem` methods. These methods map to our custom functions that manipulate search parameters. ```ts const searchParamsStorage = { getItem: (key: string) => getSearchParam(key), setItem: (key: string, value: string) => updateSearchParam(key, value), removeItem: (key: string) => removeSearchParam(key), } ``` Now, we initialize the vanilla store using the `persist` middleware, specifying that we want to use our custom storage. Instead of the default `localStorage` or `sessionStorage`, we’ll persist the position data in the URL search parameters. ```ts import { createStore } from 'zustand/vanilla' import { persist, createJSONStorage } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', storage: createJSONStorage(() => searchParamsStorage), }, ), ) ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the full code to create a dot that follows your mouse movement inside a container and persists the position in URL's search parameters. ```ts import { createStore } from 'zustand/vanilla' import { persist, createJSONStorage } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const getSearchParams = () => { return new URL(location.href).searchParams } const updateSearchParams = (searchParams: URLSearchParams) => { window.history.replaceState( {}, '', `${location.pathname}?${searchParams.toString()}`, ) } const getSearchParam = (key: string) => { const searchParams = getSearchParams() return searchParams.get(key) } const updateSearchParam = (key: string, value: string) => { const searchParams = getSearchParams() searchParams.set(key, value) updateSearchParams(searchParams) } const removeSearchParam = (key: string) => { const searchParams = getSearchParams() searchParams.delete(key) updateSearchParams(searchParams) } const searchParamsStorage = { getItem: (key: string) => getSearchParam(key), setItem: (key: string, value: string) => updateSearchParam(key, value), removeItem: (key: string) => removeSearchParam(key), } const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', storage: createJSONStorage(() => searchParamsStorage), }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Persisting a state through versioning and migrations In this tutorial, we’ll explore how to manage state persistence using versioning and migration. We will demonstrate how to evolve your state schema across versions without breaking existing persisted data. Before moving to versioned state management, we simulate an initial state for `version` 0. This is done by manually setting a `version` 0 state in `localStorage` if it doesn't already exist. The `version` 0 state saves the coordinates as `x` and `y` fields. ```ts // For tutorial purposes only if (!localStorage.getItem('position-storage')) { localStorage.setItem( 'position-storage', JSON.stringify({ state: { x: 100, y: 100 }, // version 0 structure version: 0, }), ) } ``` Next, we use `persist` middleware to handle state persistence. We also add a migration function to handle changes between versions. In this example, we `migrate` the state from `version` 0 (where `x` and `y` are separate) to `version` 1, where they are combined into a `position` object. ```ts migrate: (persisted: any, version) => { if (version === 0) { persisted.position = { x: persisted.x, y: persisted.y } delete persisted.x delete persisted.y } return persisted } ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the complete code. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' // For tutorial purposes only if (!localStorage.getItem('position-storage')) { localStorage.setItem( 'position-storage', JSON.stringify({ state: { x: 100, y: 100 }, version: 0, }), ) } type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, // version 0: just x: 0, y: 0 setPosition: (position) => set({ position }), }), { name: 'position-storage', version: 1, migrate: (persisted: any, version) => { if (version === 0) { persisted.position = { x: persisted.x, y: persisted.y } delete persisted.x delete persisted.y } return persisted }, }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Persisting a state with nested objects In this tutorial, we’ll create a vanilla store that keeps track of a position represented by `x` and `y` coordinates. We will also implement persistence using `localStorage` and demonstrate how to handle merging of state with potentially missing fields. To simulate an initial state for the tutorial, we will check if our position data exists in `localStorage`. If it doesn't, we’ll set it up. ```ts if (!localStorage.getItem('position-storage')) { localStorage.setItem( 'position-storage', JSON.stringify({ state: { position: { y: 100 } }, // missing `x` field version: 0, }), ) } ``` Now, we will create the store and configure it to use persistence and deep merging. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' import createDeepMerge from '@fastify/deepmerge' const deepMerge = createDeepMerge({ all: true }) type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', merge: (persisted, current) => deepMerge(current, persisted) as never, }, ), ) ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the complete code. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' import createDeepMerge from '@fastify/deepmerge' const deepMerge = createDeepMerge({ all: true }) // For tutorial purposes only if (!localStorage.getItem('position-storage')) { localStorage.setItem( 'position-storage', JSON.stringify({ state: { position: { y: 100 } }, // missing `x` field version: 0, }), ) } type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', merge: (persisted, current) => deepMerge(current, persisted) as never, }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { console.log({ state }) $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Persisting a state and hydrate it manually In this tutorial, we’ll create a vanilla store that keeps track of a position represented by `x` and `y` coordinates. We will also implement persistence using `localStorage` and explore how to skip the hydration process and manually trigger rehydration after a delay. We start by setting up a vanilla store that holds the position (an object with `x` and `y` coordinates) and an action to update it. Furthermore, we'll also use the `persist` middleware to store the position in `localStorage` but skipping hydration. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', skipHydration: true, }, ), ) ``` Since we skipped hydration in the initial setup, we will manually rehydrate the state. Here, we’re using `setTimeout` to simulate a delayed rehydration. ```ts setTimeout(() => { positionStore.persist.rehydrate() }, 2000) ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the complete code. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', skipHydration: true, }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } setTimeout(() => { positionStore.persist.rehydrate() }, 2000) render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ## Troubleshooting TBD ================================================ FILE: docs/reference/middlewares/redux.md ================================================ --- title: redux description: How to use actions and reducers in a store nav: 30 --- # redux `redux` middleware lets you update a store through actions and reducers just like redux. ```js const nextStateCreatorFn = redux(reducerFn, initialState) ``` - [Types](#types) - [Signature](#signature) - [Mutator](#mutator) - [Reference](#reference) - [Usage](#usage) - [Updating state through actions and reducers](#updating-state-through-actions-and-reducers) - [Troubleshooting](#troubleshooting) ## Types ### Signature ```ts redux(reducerFn: (state: T, action: A) => T, initialState: T): StateCreator A }, [['zustand/redux', A]], []> ``` ### Mutator ```ts ;['zustand/redux', A] ``` ## Reference ### `redux(reducerFn, initialState)` #### Parameters - `reducerFn`: It should be pure and should take the current state of your application and an action object as arguments, and returns the new state resulting from applying the action. - `initialState`: The value you want the state to be initially. It can be a value of any type, except a function. #### Returns `redux` returns a state creator function. ## Usage ### Updating state through actions and reducers ```ts import { createStore } from 'zustand/vanilla' import { redux } from 'zustand/middleware' type PersonStoreState = { firstName: string lastName: string email: string } type PersonStoreAction = | { type: 'person/setFirstName'; firstName: string } | { type: 'person/setLastName'; lastName: string } | { type: 'person/setEmail'; email: string } type PersonStore = PersonStoreState & { dispatch: (action: PersonStoreAction) => PersonStoreAction } const personStoreReducer = ( state: PersonStoreState, action: PersonStoreAction, ) => { switch (action.type) { case 'person/setFirstName': { return { ...state, firstName: action.firstName } } case 'person/setLastName': { return { ...state, lastName: action.lastName } } case 'person/setEmail': { return { ...state, email: action.email } } default: { return state } } } const personStoreInitialState: PersonStoreState = { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', } const personStore = createStore()( redux(personStoreReducer, personStoreInitialState), ) const $firstNameInput = document.getElementById( 'first-name', ) as HTMLInputElement const $lastNameInput = document.getElementById('last-name') as HTMLInputElement const $emailInput = document.getElementById('email') as HTMLInputElement const $result = document.getElementById('result') as HTMLDivElement function handleFirstNameChange(event: Event) { personStore.dispatch({ type: 'person/setFirstName', firstName: (event.target as any).value, }) } function handleLastNameChange(event: Event) { personStore.dispatch({ type: 'person/setLastName', lastName: (event.target as any).value, }) } function handleEmailChange(event: Event) { personStore.dispatch({ type: 'person/setEmail', email: (event.target as any).value, }) } $firstNameInput.addEventListener('input', handleFirstNameChange) $lastNameInput.addEventListener('input', handleLastNameChange) $emailInput.addEventListener('input', handleEmailChange) const render: Parameters[0] = (person) => { $firstNameInput.value = person.firstName $lastNameInput.value = person.lastName $emailInput.value = person.email $result.innerHTML = `${person.firstName} ${person.lastName} (${person.email})` } render(personStore.getInitialState(), personStore.getInitialState()) personStore.subscribe(render) ``` Here's the `html` code ```html

``` ## Troubleshooting TBD ================================================ FILE: docs/reference/middlewares/subscribe-with-selector.md ================================================ --- title: subscribeWithSelector description: How to subscribe to granular store updates in a store nav: 33 --- # subscribeWithSelector `subscribeWithSelector` middleware lets you subscribe to specific data based on current state. ```js const nextStateCreatorFn = subscribeWithSelector(stateCreatorFn) ``` - [Types](#types) - [Signature](#signature) - [Mutator](#mutator) - [Reference](#reference) - [Usage](#usage) - [Troubleshooting](#troubleshooting) ## Types ### Signature ```ts subscribeWithSelector(stateCreatorFn: StateCreator): StateCreator ``` ### Mutator ```ts ;['zustand/subscribeWithSelector', never] ``` ## Reference ### `subscribeWithSelector(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. #### Returns `subscribeWithSelector` returns a state creator function. ## Usage ### Subscribing partial state updates By subscribing to partial state updates, you register a callback that fires whenever the store's partial state updates. We can use `subscribe` for external state management. ```ts import { createStore } from 'zustand/vanilla' import { subscribeWithSelector } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( subscribeWithSelector((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })), ) const $dot = document.getElementById('dot') as HTMLDivElement $dot.addEventListener('mouseenter', (event) => { const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight positionStore.getState().setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe((state) => state.position, render) const logger: Parameters[0] = (x) => { console.log('new x position', { x }) } positionStore.subscribe((state) => state.position.x, logger) ``` Here's the `html` code ```html
``` ## Troubleshooting TBD ================================================ FILE: docs/reference/migrations/migrating-to-v4.md ================================================ --- title: Migrating to v4 nav: 38 --- The only breaking changes are in types. If you are using Zustand with TypeScript or JSDoc type annotations, this guide applies. Otherwise, no migration is required. Also, it's recommended to first read the new [TypeScript Guide](../../learn/guides/advanced-typescript.md), so that the migration is easier to understand. In addition to this migration guide, you can also check the [diff](https://github.com/pmndrs/zustand/compare/v3.7.2...v4.0.0?short_path=37e5b4c#diff-c21e24854115b390eccde717da83f91feb2d5927a76c1485e5f0fdd0135c2afa) of the test files in the Zustand repository from v3 to v4. ## `create` **Applicable imports** ```ts import create from 'zustand' import create from 'zustand/vanilla' ``` **Change** ```diff - create: - < State - , StoreSetState = StoreApi["set"] - , StoreGetState = StoreApi["get"] - , Store = StoreApi - > - (f: ...) => ... + create: + { (): (f: ...) => ... + , (f: ...) => ... + } ``` **Migration** If you are not passing any type parameters to `create`, no migration is required. If you are using a "leaf" middleware like `combine` or `redux`, remove all type parameters from `create`. Else, replace `create(...)` with `create()(...)`. ## `StateCreator` **Applicable imports** ```ts import type { StateCreator } from 'zustand' import type { StateCreator } from 'zustand/vanilla' ``` **Change** ```diff - type StateCreator - < State - , StoreSetState = StoreApi["set"] - , StoreGetState = StoreApi["get"] - , Store = StoreApi - > = - ... + type StateCreator + < State + , InMutators extends [StoreMutatorIdentifier, unknown][] = [] + , OutMutators extends [StoreMutatorIdentifier, unknown][] = [] + , Return = State + > = + ... ``` **Migration** If you are using `StateCreator`, you are likely authoring a middleware or using the "slices" pattern. For that check the [Authoring middlewares and advanced usage](../../learn/guides/advanced-typescript.md#authoring-middlewares-and-advanced-usage) and [Common recipes](../../learn/guides/advanced-typescript.md#common-recipes) sections of the TypeScript Guide. ## `PartialState` **Applicable imports** ```ts import type { PartialState } from 'zustand' import type { PartialState } from 'zustand/vanilla' ``` **Change** ```diff - type PartialState - < T extends State - , K1 extends keyof T = keyof T - , K2 extends keyof T = K1 - , K3 extends keyof T = K2 - , K4 extends keyof T = K3 - > = - | (Pick | Pick | Pick | Pick | T) - | ((state: T) => Pick | Pick | Pick | Pick | T) + type PartialState = + | Partial + | ((state: T) => Partial) ``` **Migration** Replace `PartialState` with `PartialState` and preferably turn on [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes) in your `tsconfig.json`: ```json { "compilerOptions": { "exactOptionalPropertyTypes": true } } ``` We're no longer using the trick to disallow `{ foo: undefined }` to be assigned to `Partial<{ foo: string }>`. Instead, we're relying on the users to turn on `exactOptionalPropertyTypes`. ## `useStore` **Applicable imports** ```ts import { useStore } from 'zustand' import { useStore } from 'zustand/react' ``` **Change** ```diff - useStore: - { (store: StoreApi): State - , - ( store: StoreApi - , selector: StateSelector, - , equals?: EqualityChecker - ): StateSlice - } + useStore: + > + ( store: Store + , selector?: StateSelector, + , equals?: EqualityChecker + ) + => StateSlice ``` **Migration** If you are not passing any type parameters to `useStore`, no migration is required. If you are, it's recommended to remove all the type parameters, or pass the **store** type instead of the **state** type as the first parameter. ## `UseBoundStore` **Applicable imports** ```ts import type { UseBoundStore } from 'zustand' import type { UseBoundStore } from 'zustand/react' ``` **Change** ```diff - type UseBoundStore< - State, - Store = StoreApi - > = - & { (): T - , - ( selector: StateSelector - , equals?: EqualityChecker - ): U - } - & Store + type UseBoundStore = + & (> + ( selector?: (state: ExtractState) => StateSlice + , equals?: (a: StateSlice, b: StateSlice) => boolean + ) => StateSlice + ) + & S ``` **Migration** Replace `UseBoundStore` with `UseBoundStore>`, and `UseBoundStore` with `UseBoundStore` ## `UseContextStore` **Applicable imports** ```ts import type { UseContextStore } from 'zustand/context' ``` **Change** ```diff - type UseContextStore ``` **Migration** Use `typeof MyContext.useStore` instead ## `createContext` **Applicable imports** ```ts import createContext from 'zustand/context' ``` **Change** ```diff createContext: - >() => ... + () => ... ``` **Migration** Replace `createContext()` with `createContext>()`, and `createContext()` with `createContext()`. ## `combine`, `devtools`, `subscribeWithSelector` **Applicable imports** ```ts import { combine } from 'zustand/middleware' import { devtools } from 'zustand/middleware' import { subscribeWithSelector } from 'zustand/middleware' ``` **Change** ```diff - combine: - (...) => ... + combine: + (...) => ... - devtools: - (...) => ... + devtools: + (...) => ... - subscribeWithSelector: - (...) => ... + subscribeWithSelector: + (...) => ... ``` **Migration** If you are not passing any type parameters to `combine`, `devtools`, or `subscribeWithSelector`, no migration is required. If you are, remove all the type parameters, as they are inferred automatically. ## `persist` **Applicable imports** ```ts import { persist } from 'zustand/middleware' ``` **Change** ```diff - persist: - >(...) => ... + persist: + (...) => ... ``` **Migration** If you are passing any type parameters, remove them as they are inferred automatically. Next, if you are passing the `partialize` option, there is no further steps required for migration. If you are **not** passing the `partialize` option, you might see some compilation errors. If you do not see any, there is no further migration required. The type of partialized state is now `T` instead of `Partial`, which aligns with the runtime behavior of the default `partialize`, which is an identity (`s => s`). If you see some compilation errors, you have to find and fix the errors yourself, because they might be indicative of unsound code. Alternatively, the workaround will be passing `s => s as Partial` to `partialize`. If your partialized state is truly `Partial`, you should not encounter any bugs. The runtime behavior has not changed, only the types are now correct. ## `redux` **Applicable imports** ```ts import { redux } from 'zustand/middleware' ``` **Change** ```diff - redux: - (...) => ... + redux: + (...) => ... ``` **Migration** If you are not passing any type parameters to `redux`, no migration is required. If you are, remove all the type parameters, and annotate only the second (action) parameter. That is, replace `redux((state, action) => ..., ...)` with `redux((state, action: A) => ..., ...)`. ================================================ FILE: docs/reference/migrations/migrating-to-v5.md ================================================ --- title: How to Migrate to v5 from v4 nav: 37 --- # How to Migrate to v5 from v4 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. ## Changes in v5 - Drop default exports - Drop deprecated features - Make React 18 the minimum required version - Make use-sync-external-store a peer dependency (required for `createWithEqualityFn` and `useStoreWithEqualityFn` in `zustand/traditional`) - Make TypeScript 4.5 the minimum required version - Drop UMD/SystemJS support - Organize entry points in the package.json - Drop ES5 support - Stricter types when setState's replace flag is set - Persist middleware behavioral change - Other small improvements (technically breaking changes) ## Migration Guide ### Using custom equality functions such as `shallow` The `create` function in v5 does not support customizing equality function. If you use custom equality function such as `shallow`, the easiest migration is to use `createWithEqualityFn`. ```js // v4 import { create } from 'zustand' import { shallow } from 'zustand/shallow' const useCountStore = create((set) => ({ count: 0, text: 'hello', // ... })) const Component = () => { const { count, text } = useCountStore( (state) => ({ count: state.count, text: state.text, }), shallow, ) // ... } ``` That can be done with `createWithEqualityFn` in v5: ```bash npm install use-sync-external-store ``` ```js // v5 import { createWithEqualityFn as create } from 'zustand/traditional' // The rest is the same as v4 ``` Alternatively, for the `shallow` use case, you can use `useShallow` hook: ```js // v5 import { create } from 'zustand' import { useShallow } from 'zustand/shallow' const useCountStore = create((set) => ({ count: 0, text: 'hello', // ... })) const Component = () => { const { count, text } = useCountStore( useShallow((state) => ({ count: state.count, text: state.text, })), ) // ... } ``` ### Requiring stable selector outputs There is a behavioral change in v5 to match React default behavior. If a selector returns a new reference, it may cause infinite loops. For example, this may cause infinite loops. ```js // v4 const [searchValue, setSearchValue] = useStore((state) => [ state.searchValue, state.setSearchValue, ]) ``` The error message will be something like this: ```plaintext 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. ``` To fix it, use the `useShallow` hook, which will return a stable reference. ```js // v5 import { useShallow } from 'zustand/shallow' const [searchValue, setSearchValue] = useStore( useShallow((state) => [state.searchValue, state.setSearchValue]), ) ``` Here's another example that may cause infinite loops. ```js // v4 const action = useMainStore((state) => { return state.action ?? () => {} }) ``` To fix it, make sure the selector function returns a stable reference. ```js // v5 const FALLBACK_ACTION = () => {} const action = useMainStore((state) => { return state.action ?? FALLBACK_ACTION }) ``` Alternatively, if you need v4 behavior, `createWithEqualityFn` will do. ```js // v5 import { createWithEqualityFn as create } from 'zustand/traditional' ``` ### Stricter types when setState's replace flag is set (Typescript only) ```diff - setState: - (partial: T | Partial | ((state: T) => T | Partial), replace?: boolean | undefined) => void; + setState: + (partial: T | Partial | ((state: T) => T | Partial), replace?: false) => void; + (state: T | ((state: T) => T), replace: true) => void; ``` If you are not using the `replace` flag, no migration is required. If you are using the `replace` flag and it's set to `true`, you must provide a complete state object. This change ensures that `store.setState({}, true)` (which results in an invalid state) is no longer considered valid. **Examples:** ```ts // Partial state update (valid) store.setState({ key: 'value' }) // Complete state replacement (valid) store.setState({ key: 'value' }, true) // Incomplete state replacement (invalid) store.setState({}, true) // Error ``` #### Handling Dynamic `replace` Flag 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: ```ts const replaceFlag = Math.random() > 0.5 const args = [{ bears: 5 }, replaceFlag] as Parameters< typeof useBearStore.setState > store.setState(...args) ``` #### Persist middleware no longer stores item at store creation Previously, the `persist` middleware stored the initial state during store creation. This behavior has been removed in v5 (and v4.5.5). For example, in the following code, the initial state is stored in the storage. ```js // v4 import { create } from 'zustand' import { persist } from 'zustand/middleware' const useCountStore = create( persist( () => ({ count: Math.floor(Math.random() * 1000), }), { name: 'count', }, ), ) ``` In v5, this is no longer the case, and you need to explicitly set the state after store creation. ```js // v5 import { create } from 'zustand' import { persist } from 'zustand/middleware' const useCountStore = create( persist( () => ({ count: 0, }), { name: 'count', }, ), ) useCountStore.setState({ count: Math.floor(Math.random() * 1000), }) ``` ## Links - https://github.com/pmndrs/zustand/pull/2138 - https://github.com/pmndrs/zustand/pull/2580 ================================================ FILE: docs/reference/previous-versions/zustand-v3-create-context.md ================================================ --- title: createContext from zustand/context nav: 39 --- A special `createContext` is provided since v3.5, which avoids misusing the store hook. > **Note**: This function is deprecated in v4 and will be removed in v5. See [Migration](#migration). ```jsx import create from 'zustand' import createContext from 'zustand/context' const { Provider, useStore } = createContext() const createStore = () => create(...) const App = () => ( ... ) const Component = () => { const state = useStore() const slice = useStore(selector) ... ``` ## createContext usage in real components ```jsx import create from "zustand"; import createContext from "zustand/context"; // 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. const { Provider, useStore } = createContext(); const createStore = () => create((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }) })); const Button = () => { return ( {/** store() - This will create a store for each time using the Button component instead of using one store for all components **/} ); }; const ButtonChild = () => { const state = useStore(); return (
{state.bears}
); }; export default function App() { return (
); } ``` ## createContext usage with initialization from props ```tsx import create from 'zustand' import createContext from 'zustand/context' const { Provider, useStore } = createContext() export default function App({ initialBears }) { return ( create((set) => ({ bears: initialBears, increase: () => set((state) => ({ bears: state.bears + 1 })), })) } > ) } export default function App() { return ( <>
) } ================================================ FILE: examples/demo/src/components/CodePreview.jsx ================================================ import { create } from 'zustand' import { Highlight } from 'prism-react-renderer' import CopyButton from './CopyButton' import SnippetLang from './SnippetLang' import javascriptCode from '../resources/javascript-code' import typescriptCode from '../resources/typescript-code' const useStore = create((set, get) => ({ lang: 'javascript', setLang: (lang) => set(() => ({ lang })), getCode: () => get().lang === 'javascript' ? javascriptCode : typescriptCode, })) export default function CodePreview() { const { lang, setLang, getCode } = useStore() const code = getCode() return ( {({ className, style, tokens, getLineProps, getTokenProps }) => ( // define how each line is to be rendered in the code block, // position is set to relative so the copy button can align to bottom right
          {tokens.map((line, i) => (
            
{line.map((token, key) => ( ))}
))}
)}
) } ================================================ FILE: examples/demo/src/components/CopyButton.jsx ================================================ import { useState, useCallback, useRef } from 'react' import { copyToClipboard } from '../utils/copy-to-clipboard' /* Isolated logic for the entire copy functionality instead of a separate button component and with the added utility */ export default function CopyButton({ code, ...props }) { const [isCopied, setIsCopied] = useState(false) const timer = useRef() const handleCopy = useCallback(() => { clearTimeout(timer.current) copyToClipboard(code).then(() => { setIsCopied(true) timer.current = setTimeout(() => setIsCopied(false), 3000) }) }, [code]) return ( <> ) } ================================================ FILE: examples/demo/src/components/Details.jsx ================================================ export default function Details() { return ( <> Zustand ) } ================================================ FILE: examples/demo/src/components/Fireflies.jsx ================================================ import { Vector3, CatmullRomCurve3 } from 'three' import { useRef, useMemo } from 'react' import { extend, useFrame } from '@react-three/fiber' import * as meshline from 'meshline' extend(meshline) const r = () => Math.max(0.2, Math.random()) function Fatline({ curve, color }) { const material = useRef() useFrame( (state, delta) => (material.current.uniforms.dashOffset.value -= delta / 100), ) return ( ) } export default function Fireflies({ count, colors, radius = 10 }) { const lines = useMemo( () => new Array(count).fill().map(() => { const pos = new Vector3( Math.sin(0) * radius * r(), Math.cos(0) * radius * r(), 0, ) const points = new Array(30).fill().map((_, index) => { const angle = (index / 20) * Math.PI * 2 return pos .add( new Vector3( Math.sin(angle) * radius * r(), Math.cos(angle) * radius * r(), 0, ), ) .clone() }) const curve = new CatmullRomCurve3(points).getPoints(100) return { color: colors[parseInt(colors.length * Math.random())], curve, } }), [count, radius, colors], ) return ( {lines.map((props, index) => ( ))} ) } ================================================ FILE: examples/demo/src/components/Scene.jsx ================================================ import { Mesh, PlaneGeometry, Group, Vector3, MathUtils } from 'three' import { useRef, useState, useLayoutEffect } from 'react' import { createRoot, events, extend, useFrame } from '@react-three/fiber' import { Plane, useAspect, useTexture } from '@react-three/drei' import { EffectComposer, DepthOfField, Vignette, } from '@react-three/postprocessing' import { MaskFunction } from 'postprocessing' import Fireflies from './Fireflies' import bgUrl from '../resources/bg.jpg' import starsUrl from '../resources/stars.png' import groundUrl from '../resources/ground.png' import bearUrl from '../resources/bear.png' import leaves1Url from '../resources/leaves1.png' import leaves2Url from '../resources/leaves2.png' import '../materials/layerMaterial' function Experience() { const scaleN = useAspect(1600, 1000, 1.05) const scaleW = useAspect(2200, 1000, 1.05) const textures = useTexture([ bgUrl, starsUrl, groundUrl, bearUrl, leaves1Url, leaves2Url, ]) const group = useRef() const layersRef = useRef([]) const [movement] = useState(() => new Vector3()) const [temp] = useState(() => new Vector3()) const layers = [ { texture: textures[0], x: 0, y: 0, z: 0, factor: 0.005, scale: scaleW }, { texture: textures[1], x: 0, y: 0, z: 10, factor: 0.005, scale: scaleW }, { texture: textures[2], x: 0, y: 0, z: 20, scale: scaleW }, { texture: textures[3], x: 0, y: 0, z: 30, scaleFactor: 0.83, scale: scaleN, }, { texture: textures[4], x: 0, y: 0, z: 40, factor: 0.03, scaleFactor: 1, wiggle: 0.6, scale: scaleW, }, { texture: textures[5], x: -20, y: -20, z: 49, factor: 0.04, scaleFactor: 1.3, wiggle: 1, scale: scaleW, }, ] useFrame((state, delta) => { movement.lerp(temp.set(state.pointer.x, state.pointer.y * 0.2, 0), 0.2) group.current.position.x = MathUtils.lerp( group.current.position.x, state.pointer.x * 20, 0.05, ) group.current.rotation.x = MathUtils.lerp( group.current.rotation.x, state.pointer.y / 20, 0.05, ) group.current.rotation.y = MathUtils.lerp( group.current.rotation.y, -state.pointer.x / 2, 0.05, ) layersRef.current[4].uniforms.time.value = layersRef.current[5].uniforms.time.value += delta }, 1) return ( {layers.map( ( { scale, texture, ref, factor = 0, scaleFactor = 1, wiggle = 0, x, y, z, }, i, ) => ( (layersRef.current[i] = el)} wiggle={wiggle} scale={scaleFactor} /> ), )} ) } function Effects() { const ref = useRef() useLayoutEffect(() => { const maskMaterial = ref.current.maskPass.getFullscreenMaterial() maskMaterial.maskFunction = MaskFunction.MULTIPLY_RGB_SET_ALPHA }) return ( ) } function FallbackScene() { return (
Zustand Bear
) } export default function Scene() { const [error, setError] = useState(null) if (error) { return } return ( ) } function Canvas({ children, onError }) { extend({ Mesh, PlaneGeometry, Group }) const canvas = useRef(null) const root = useRef(null) useLayoutEffect(() => { try { if (!root.current) { root.current = createRoot(canvas.current).configure({ events, orthographic: true, gl: { antialias: false }, camera: { zoom: 5, position: [0, 0, 200], far: 300, near: 50 }, onCreated: (state) => { state.events.connect(document.getElementById('root')) state.setEvents({ compute: (event, state) => { state.pointer.set( (event.clientX / state.size.width) * 2 - 1, -(event.clientY / state.size.height) * 2 + 1, ) state.raycaster.setFromCamera(state.pointer, state.camera) }, }) }, }) } const resize = () => root.current.configure({ width: window.innerWidth, height: window.innerHeight, }) window.addEventListener('resize', resize) root.current.render(children) return () => window.removeEventListener('resize', resize) } catch (e) { onError?.(e) } }, [children, onError]) return ( ) } ================================================ FILE: examples/demo/src/components/SnippetLang.jsx ================================================ export default function SnippetLang({ lang, setLang }) { return ( ) } ================================================ FILE: examples/demo/src/main.jsx ================================================ import { createRoot } from 'react-dom/client' import './styles.css' import './pmndrs.css' import App from './App' createRoot(document.getElementById('root')).render() ================================================ FILE: examples/demo/src/materials/layerMaterial.js ================================================ import { shaderMaterial } from '@react-three/drei' import { extend } from '@react-three/fiber' // This material takes care of wiggling and punches a hole into // alpha regions so that the depth-of-field effect can process the layers. // Credit: Gianmarco Simone https://twitter.com/ggsimm const LayerMaterial = shaderMaterial( { textr: null, movement: [0, 0, 0], scale: 1, factor: 0, wiggle: 0, time: 0 }, ` uniform float time; uniform vec2 resolution; uniform float wiggle; varying vec2 vUv; varying vec3 vNormal; void main() { vUv = uv; vec3 transformed = vec3(position); if (wiggle > 0.) { float theta = sin(time + position.y) / 2.0 * wiggle; float c = cos(theta); float s = sin(theta); mat3 m = mat3(c, 0, s, 0, 1, 0, -s, 0, c); transformed = transformed * m; vNormal = vNormal * m; } gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.); }`, ` uniform float time; uniform vec2 resolution; uniform float factor; uniform float scale; uniform vec3 movement; uniform sampler2D textr; varying vec2 vUv; void main() { vec2 uv = vUv / scale + movement.xy * factor; vec4 color = texture2D(textr, uv); if (color.a < 0.1) discard; gl_FragColor = vec4(color.rgb, .1); #include #include }`, ) extend({ LayerMaterial }) ================================================ FILE: examples/demo/src/pmndrs.css ================================================ /** * Pmndrs theme for JavaScript, CSS and HTML * Loosely based on https://marketplace.visualstudio.com/items?itemName=pmndrs.pmndrs * @author Paul Henschel */ code[class*='language-'], pre[class*='language-'] { color: #e4f0fb !important; background: none !important; text-shadow: 0 1px rgba(0, 0, 0, 0.3) !important; font-family: Menlo, Monaco, 'Courier New', monospace !important; font-size: 0.95em !important; text-align: left !important; white-space: pre !important; word-spacing: normal !important; word-break: normal !important; word-wrap: normal !important; line-height: 1.5 !important; -moz-tab-size: 4 !important; -o-tab-size: 4 !important; tab-size: 4 !important; -webkit-hyphens: none !important; -moz-hyphens: none !important; -ms-hyphens: none !important; hyphens: none !important; } /* Code blocks */ pre[class*='language-'] { padding: 3.75em !important; margin: -2.5em 0 !important; overflow: auto !important; border-radius: 0.75em !important; } :not(pre) > code[class*='language-'], pre[class*='language-'] { background: #252b37 !important; } /* Inline code */ :not(pre) > code[class*='language-'] { padding: 0.1em !important; border-radius: 0.3em !important; white-space: normal !important; } .token.comment, .token.prolog, .token.doctype, .token.cdata { color: #a6accd !important; } .token.punctuation { color: #e4f0fb !important; } .token.namespace { opacity: 0.7 !important; } .token.property, .token.tag, .token.constant, .token.symbol, .token.deleted { color: #e4f0fb !important; } .token.boolean, .token.number { color: #5de4c7 !important; } .token.selector, .token.attr-value, .token.string, .token.char, .token.builtin, .token.inserted { color: #5de4c7 !important; } .token.attr-name, .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string, .token.variable { color: #add7ff !important; } .token.atrule, .token.function, .token.class-name { color: #5de4c7 !important; } .token.keyword { color: #add7ff !important; } .token.regex, .token.important { color: #fffac2 !important; } .token.important, .token.bold { font-weight: bold !important; } .token.italic { font-style: italic !important; } .token.entity { cursor: help !important; } ================================================ FILE: examples/demo/src/resources/javascript-code.js ================================================ export default `import { create } from 'zustand' const useStore = create((set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), })) function Counter() { const { count, inc } = useStore() return (
{count}
) }` ================================================ FILE: examples/demo/src/resources/typescript-code.js ================================================ export default `import { create } from 'zustand' type Store = { count: number inc: () => void } const useStore = create()((set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), })) function Counter() { const { count, inc } = useStore() return (
{count}
) }` ================================================ FILE: examples/demo/src/styles.css ================================================ * { box-sizing: border-box; } html, body, #root { width: 100%; height: 100%; margin: 0; padding: 0; -webkit-touch-callout: none; overflow: hidden; background: #010101; } #root { overflow: hidden; } body { font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif; } .main { position: absolute; top: 0; left: 0; width: 100%; height: 100%; color: white; } .main > .code { position: absolute; right: 10vw; margin-right: -60px; width: 640px; max-width: 80%; height: 100%; display: flex; align-items: center; justify-content: center; } .code-container { position: relative; margin-bottom: -60px; } .counter { position: absolute; top: -100px; right: -20px; color: white; background: #394a52; padding: 40px; border-radius: 10px; box-shadow: 0 16px 40px -5px rgba(0, 0, 0, 0.5); width: 120px; height: 120px; font-size: 3em; } .counter > span { position: absolute; left: 50%; top: 50%; margin-top: -15px; transform: translate3d(-50%, -50%, 0); } .counter > button { margin: 10px; padding: 5px 10px; position: absolute; left: 0; bottom: 0; width: 100px; border-radius: 5px; border: solid 2px white; outline: none; background: transparent; color: white; cursor: pointer; } .code-container pre[class*='language-'] { margin-top: -50px; display: inline-block; width: auto !important; padding: 40px 50px 40px 45px; font-size: 0.8rem !important; border-radius: 10px !important; box-shadow: 0 16px 40px -5px rgba(0, 0, 0, 1); white-space: pre-wrap !important; } span.header-left { font-weight: 700; text-transform: uppercase; position: absolute; display: inline-block; top: 40px; left: 40px; font-size: 3em; color: white; line-height: 1em; } a { font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif; font-weight: 400; font-size: 16px; color: inherit; position: absolute; display: inline; text-decoration: none; } .nav { align-items: center; display: flex; gap: 16px; justify-content: flex-end; left: 40px; position: fixed; right: 40px; top: 40px; } .nav a { position: relative; flex: 0 0 auto; } .bottom { position: fixed; bottom: 40px; left: 40px; right: 40px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; } .bottom a { position: static; } .bottom-links { display: flex; gap: 16px; align-items: center; } .snippet-container { display: flex; align-items: center; gap: 4px; position: absolute; bottom: 0; right: 0; padding: 5px; } .snippet-lang { background-color: #272822; color: #fff; outline: 0; border: 0; } .copy-button { box-shadow: none; text-decoration: none; font-size: 14px; font-family: sans-serif; line-height: 1; padding: 12px; width: auto; border-radius: 5px; border: 0; outline: none; background: transparent; color: #f8f9fa; cursor: pointer; } .copy-button:hover { background-color: #5f5e5d; } @media only screen and (max-width: 700px) { span.header-left { font-size: 1em; } .main > .code { margin-right: -0px; } .code-container > pre[class*='language-'] { font-size: 0.6rem !important; border-radius: 10px 10px 0 0 !important; } .counter { position: absolute; top: -120px; } } ================================================ FILE: examples/demo/src/utils/copy-to-clipboard.js ================================================ export const copyToClipboard = (str) => { return navigator.clipboard.writeText(str) } ================================================ FILE: examples/demo/vite.config.js ================================================ import react from '@vitejs/plugin-react-swc' import { defineConfig } from 'vite' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/starter/README.md ================================================ # 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) ## Set up locally ```bash git clone https://github.com/pmndrs/zustand # install project dependencies & build the library cd zustand && pnpm install # move to the examples folder & install dependencies cd examples/starter && pnpm install # start the dev server pnpm dev ``` ## Set up on `StackBlitz` Link: https://stackblitz.com/github/pmndrs/zustand/tree/main/examples/starter ================================================ FILE: examples/starter/index.html ================================================ Zustand Examples | Starter
================================================ FILE: examples/starter/package.json ================================================ { "name": "starter", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" }, "dependencies": { "zustand": "^5.0.2", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react-swc": "^3.5.0", "typescript": "^5.0.0", "vite": "^5.3.4" } } ================================================ FILE: examples/starter/src/index.css ================================================ html, body, #root { height: 100%; } #root { display: flex; place-items: center; justify-content: center; color: #fff; background-color: #131311; } ================================================ FILE: examples/starter/src/index.tsx ================================================ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { create } from 'zustand' import mascot from './assets/zustand-mascot.svg' import './index.css' type Store = { count: number inc: () => void } const useStore = create((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) const Counter = () => { const count = useStore((s) => s.count) const inc = useStore((s) => s.inc) return ( <> {count} ) } function App() { return (
Zustand mascot

Zustand Starter

) } createRoot(document.getElementById('root')!).render( , ) ================================================ FILE: examples/starter/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/starter/tsconfig.json ================================================ { "compilerOptions": { "target": "es2019", "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "lib": ["dom", "dom.iterable", "esnext"], "jsx": "react-jsx", "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": ["vite.config.ts", "./src/**/*"], "exclude": ["node_modules"] } ================================================ FILE: examples/starter/vite.config.ts ================================================ import react from '@vitejs/plugin-react-swc' import { defineConfig } from 'vite' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) ================================================ FILE: package.json ================================================ { "name": "zustand", "description": "🐻 Bear necessities for state management in React", "private": true, "type": "commonjs", "version": "5.0.12", "main": "./index.js", "types": "./index.d.ts", "typesVersions": { ">=4.5": { "esm/*": [ "esm/*" ], "*": [ "*" ] }, "*": { "esm/*": [ "ts_version_4.5_and_above_is_required.d.ts" ], "*": [ "ts_version_4.5_and_above_is_required.d.ts" ] } }, "exports": { "./package.json": "./package.json", ".": { "react-native": { "types": "./index.d.ts", "default": "./index.js" }, "import": { "types": "./esm/index.d.mts", "default": "./esm/index.mjs" }, "default": { "types": "./index.d.ts", "default": "./index.js" } }, "./*": { "react-native": { "types": "./*.d.ts", "default": "./*.js" }, "import": { "types": "./esm/*.d.mts", "default": "./esm/*.mjs" }, "default": { "types": "./*.d.ts", "default": "./*.js" } } }, "files": [ "**" ], "sideEffects": false, "scripts": { "prebuild": "shx rm -rf dist", "build": "pnpm run prebuild && pnpm run \"/^build:.*/\" && pnpm run postbuild", "build-watch": "pnpm run \"/^build:.*/\" --watch", "build:base": "rollup -c", "build:vanilla": "rollup -c --config-vanilla", "build:react": "rollup -c --config-react", "build:middleware": "rollup -c --config-middleware", "build:middleware:immer": "rollup -c --config-middleware_immer", "build:shallow": "rollup -c --config-shallow", "build:vanilla:shallow": "rollup -c --config-vanilla_shallow", "build:react:shallow": "rollup -c --config-react_shallow", "build:traditional": "rollup -c --config-traditional", "postbuild": "pnpm run patch-d-ts && pnpm run copy && pnpm run patch-old-ts && pnpm run patch-esm-ts", "fix": "pnpm run fix:lint && pnpm run fix:format", "fix:format": "prettier . --write", "fix:lint": "eslint . --fix", "test": "pnpm run \"/^test:.*/\"", "test:format": "prettier . --list-different", "test:types": "tsc --noEmit", "test:lint": "eslint .", "test:spec": "vitest run", "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); });\"", "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;\"", "patch-old-ts": "shx touch dist/ts_version_4.5_and_above_is_required.d.ts", "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)})\"" }, "engines": { "node": ">=12.20.0" }, "prettier": { "semi": false, "singleQuote": true }, "repository": { "type": "git", "url": "git+https://github.com/pmndrs/zustand.git" }, "keywords": [ "react", "state", "manager", "management", "redux", "store" ], "author": "Paul Henschel", "contributors": [ "Jeremy Holcomb (https://github.com/JeremyRH)", "Daishi Kato (https://github.com/dai-shi)" ], "license": "MIT", "bugs": { "url": "https://github.com/pmndrs/zustand/issues" }, "homepage": "https://github.com/pmndrs/zustand", "packageManager": "pnpm@10.18.3", "devDependencies": { "@eslint/js": "^9.39.2", "@redux-devtools/extension": "^4.0.0", "@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-typescript": "^12.3.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/use-sync-external-store": "^1.5.0", "@vitest/coverage-v8": "^4.1.0", "@vitest/eslint-plugin": "^1.6.12", "@vitest/ui": "^4.1.0", "esbuild": "^0.27.4", "eslint": "^9.39.2", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jest-dom": "^5.5.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-testing-library": "^7.16.0", "immer": "^11.1.4", "jsdom": "^27.4.0", "json": "^11.0.0", "prettier": "^3.8.1", "react": "^19.2.4", "react-dom": "^19.2.4", "redux": "^5.0.1", "rollup": "^4.59.0", "rollup-plugin-esbuild": "^6.2.1", "shelljs": "^0.10.0", "shx": "^0.4.0", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.57.0", "use-sync-external-store": "^1.6.0", "vitest": "^4.1.0" }, "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true }, "immer": { "optional": true }, "react": { "optional": true }, "use-sync-external-store": { "optional": true } } } ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - . minimumReleaseAge: 1440 ================================================ FILE: rollup.config.mjs ================================================ /* global process*/ import path from 'path' import alias from '@rollup/plugin-alias' import resolve from '@rollup/plugin-node-resolve' import replace from '@rollup/plugin-replace' import typescript from '@rollup/plugin-typescript' import esbuild from 'rollup-plugin-esbuild' const extensions = ['.js', '.ts', '.tsx'] const { root } = path.parse(process.cwd()) export const entries = [ { find: /.*\/vanilla\/shallow\.ts$/, replacement: 'zustand/vanilla/shallow' }, { find: /.*\/react\/shallow\.ts$/, replacement: 'zustand/react/shallow' }, { find: /.*\/vanilla\.ts$/, replacement: 'zustand/vanilla' }, { find: /.*\/react\.ts$/, replacement: 'zustand/react' }, ] function external(id) { return !id.startsWith('.') && !id.startsWith(root) } function getEsbuild() { return esbuild({ target: 'es2018', supported: { 'import-meta': true }, tsconfig: path.resolve('./tsconfig.json'), }) } function createDeclarationConfig(input, output) { return { input, output: { dir: output, }, external, plugins: [ typescript({ declaration: true, emitDeclarationOnly: true, outDir: output, }), ], } } function createESMConfig(input, output) { return { input, output: { file: output, format: 'esm' }, external, plugins: [ alias({ entries: entries.filter((entry) => !entry.find.test(input)) }), resolve({ extensions }), replace({ ...(output.endsWith('.js') ? { 'import.meta.env?.MODE': 'process.env.NODE_ENV', } : { 'import.meta.env?.MODE': '(import.meta.env ? import.meta.env.MODE : undefined)', }), // a workaround for #829 'use-sync-external-store/shim/with-selector': 'use-sync-external-store/shim/with-selector.js', delimiters: ['\\b', '\\b(?!(\\.|/))'], preventAssignment: true, }), getEsbuild(), ], } } function createCommonJSConfig(input, output) { return { input, output: { file: output, format: 'cjs' }, external, plugins: [ alias({ entries: entries.filter((entry) => !entry.find.test(input)) }), resolve({ extensions }), replace({ 'import.meta.env?.MODE': 'process.env.NODE_ENV', delimiters: ['\\b', '\\b(?!(\\.|/))'], preventAssignment: true, }), getEsbuild(), ], } } export default function (args) { let c = Object.keys(args).find((key) => key.startsWith('config-')) if (c) { c = c.slice('config-'.length).replace(/_/g, '/') } else { c = 'index' } return [ ...(c === 'index' ? [createDeclarationConfig(`src/${c}.ts`, 'dist')] : []), createCommonJSConfig(`src/${c}.ts`, `dist/${c}.js`), createESMConfig(`src/${c}.ts`, `dist/esm/${c}.mjs`), ] } ================================================ FILE: src/index.ts ================================================ export * from './vanilla.ts' export * from './react.ts' ================================================ FILE: src/middleware/combine.ts ================================================ import type { StateCreator, StoreMutatorIdentifier } from '../vanilla.ts' type Write = Omit & U export function combine< T extends object, U extends object, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], >( initialState: T, create: StateCreator, ): StateCreator, Mps, Mcs> { return (...args) => Object.assign({}, initialState, (create as any)(...args)) } ================================================ FILE: src/middleware/devtools.ts ================================================ import type {} from '@redux-devtools/extension' import type { StateCreator, StoreApi, StoreMutatorIdentifier, } from '../vanilla.ts' type Config = Parameters< (Window extends { __REDUX_DEVTOOLS_EXTENSION__?: infer T } ? T : { connect: (param: any) => unknown })['connect'] >[0] declare module '../vanilla' { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface StoreMutators { 'zustand/devtools': WithDevtools } } // FIXME https://github.com/reduxjs/redux-devtools/issues/1097 type Message = { type: string payload?: any state?: any } type WithDispatch = { dispatch: (...args: unknown[]) => void dispatchFromDevtools: unknown } const shouldDispatchFromDevtools = (api: unknown): api is WithDispatch => !!(api as WithDispatch).dispatchFromDevtools && typeof (api as WithDispatch).dispatch === 'function' type Cast = T extends U ? T : U type Write = Omit & U type TakeTwo = T extends { length: 0 } ? [undefined, undefined] : T extends { length: 1 } ? [...args0: Cast, arg1: undefined] : T extends { length: 0 | 1 } ? [...args0: Cast, arg1: undefined] : T extends { length: 2 } ? T : T extends { length: 1 | 2 } ? T : T extends { length: 0 | 1 | 2 } ? T : T extends [infer A0, infer A1, ...unknown[]] ? [A0, A1] : T extends [infer A0, (infer A1)?, ...unknown[]] ? [A0, A1?] : T extends [(infer A0)?, (infer A1)?, ...unknown[]] ? [A0?, A1?] : never type WithDevtools = Write> type Action = | string | { type: string [x: string | number | symbol]: unknown } type StoreDevtools = S extends { setState: { // capture both overloads of setState (...args: infer Sa1): infer Sr1 (...args: infer Sa2): infer Sr2 } } ? { setState(...args: [...args: TakeTwo, action?: Action]): Sr1 setState(...args: [...args: TakeTwo, action?: Action]): Sr2 devtools: { cleanup: () => void } } : never export interface DevtoolsOptions extends Config { name?: string enabled?: boolean anonymousActionType?: string store?: string } type Devtools = < T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], U = T, >( initializer: StateCreator, devtoolsOptions?: DevtoolsOptions, ) => StateCreator declare module '../vanilla' { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface StoreMutators { 'zustand/devtools': WithDevtools } } type DevtoolsImpl = ( storeInitializer: StateCreator, devtoolsOptions?: DevtoolsOptions, ) => StateCreator export type NamedSet = WithDevtools>['setState'] type Connection = ReturnType< NonNullable['connect'] > type ConnectionName = string | undefined type StoreName = string type StoreInformation = StoreApi type ConnectionInformation = { connection: Connection stores: Record } const trackedConnections: Map = new Map() const getTrackedConnectionState = ( name: string | undefined, ): Record => { const api = trackedConnections.get(name) if (!api) return {} return Object.fromEntries( Object.entries(api.stores).map(([key, api]) => [key, api.getState()]), ) } const extractConnectionInformation = ( store: string | undefined, extensionConnector: NonNullable< (typeof window)['__REDUX_DEVTOOLS_EXTENSION__'] >, options: Omit, ) => { if (store === undefined) { return { type: 'untracked' as const, connection: extensionConnector.connect(options), } } const existingConnection = trackedConnections.get(options.name) if (existingConnection) { return { type: 'tracked' as const, store, ...existingConnection } } const newConnection: ConnectionInformation = { connection: extensionConnector.connect(options), stores: {}, } trackedConnections.set(options.name, newConnection) return { type: 'tracked' as const, store, ...newConnection } } const removeStoreFromTrackedConnections = ( name: string | undefined, store: string | undefined, ) => { if (store === undefined) return const connectionInfo = trackedConnections.get(name) if (!connectionInfo) return delete connectionInfo.stores[store] if (Object.keys(connectionInfo.stores).length === 0) { trackedConnections.delete(name) } } const findCallerName = (stack: string | undefined) => { if (!stack) return undefined const traceLines = stack.split('\n') const apiSetStateLineIndex = traceLines.findIndex((traceLine) => traceLine.includes('api.setState'), ) if (apiSetStateLineIndex < 0) return undefined const callerLine = traceLines[apiSetStateLineIndex + 1]?.trim() || '' return /.+ (.+) .+/.exec(callerLine)?.[1] } const devtoolsImpl: DevtoolsImpl = (fn, devtoolsOptions = {}) => (set, get, api) => { const { enabled, anonymousActionType, store, ...options } = devtoolsOptions type S = ReturnType & { [store: string]: ReturnType } type PartialState = Partial | ((s: S) => Partial) let extensionConnector: | (typeof window)['__REDUX_DEVTOOLS_EXTENSION__'] | false try { extensionConnector = (enabled ?? import.meta.env?.MODE !== 'production') && window.__REDUX_DEVTOOLS_EXTENSION__ } catch { // ignored } if (!extensionConnector) { return fn(set, get, api) } const { connection, ...connectionInformation } = extractConnectionInformation(store, extensionConnector, options) let isRecording = true api.setState = ((state, replace, nameOrAction: Action) => { const r = set(state, replace as any) if (!isRecording) return r const action: { type: string } = nameOrAction === undefined ? { type: anonymousActionType || findCallerName(new Error().stack) || 'anonymous', } : typeof nameOrAction === 'string' ? { type: nameOrAction } : nameOrAction if (store === undefined) { connection?.send(action, get()) return r } connection?.send( { ...action, type: `${store}/${action.type}`, }, { ...getTrackedConnectionState(options.name), [store]: api.getState(), }, ) return r }) as NamedSet ;(api as StoreApi & StoreDevtools).devtools = { cleanup: () => { if ( connection && typeof (connection as any).unsubscribe === 'function' ) { ;(connection as any).unsubscribe() } removeStoreFromTrackedConnections(options.name, store) }, } const setStateFromDevtools: StoreApi['setState'] = (...a) => { const originalIsRecording = isRecording isRecording = false set(...(a as Parameters)) isRecording = originalIsRecording } const initialState = fn(api.setState, get, api) if (connectionInformation.type === 'untracked') { connection?.init(initialState) } else { connectionInformation.stores[connectionInformation.store] = api connection?.init( Object.fromEntries( Object.entries(connectionInformation.stores).map(([key, store]) => [ key, key === connectionInformation.store ? initialState : store.getState(), ]), ), ) } if (shouldDispatchFromDevtools(api)) { let didWarnAboutReservedActionType = false const originalDispatch = api.dispatch api.dispatch = (...args: any[]) => { if ( import.meta.env?.MODE !== 'production' && args[0].type === '__setState' && !didWarnAboutReservedActionType ) { console.warn( '[zustand devtools middleware] "__setState" action type is reserved ' + 'to set state from the devtools. Avoid using it.', ) didWarnAboutReservedActionType = true } originalDispatch(...args) } } ;( connection as unknown as { // FIXME https://github.com/reduxjs/redux-devtools/issues/1097 subscribe: ( listener: (message: Message) => void, ) => (() => void) | undefined } ).subscribe((message) => { switch (message.type) { case 'ACTION': if (typeof message.payload !== 'string') { console.error( '[zustand devtools middleware] Unsupported action format', ) return } return parseJsonThen<{ type: unknown; state?: PartialState }>( message.payload, (action) => { if (action.type === '__setState') { if (store === undefined) { setStateFromDevtools(action.state as PartialState) return } if (Object.keys(action.state as S).length !== 1) { console.error( ` [zustand devtools middleware] Unsupported __setState action format. When using 'store' option in devtools(), the 'state' should have only one key, which is a value of 'store' that was passed in devtools(), and value of this only key should be a state object. Example: { "type": "__setState", "state": { "abc123Store": { "foo": "bar" } } } `, ) } const stateFromDevtools = (action.state as S)[store] if ( stateFromDevtools === undefined || stateFromDevtools === null ) { return } if ( JSON.stringify(api.getState()) !== JSON.stringify(stateFromDevtools) ) { setStateFromDevtools(stateFromDevtools) } return } if (shouldDispatchFromDevtools(api)) { api.dispatch(action) } }, ) case 'DISPATCH': switch (message.payload.type) { case 'RESET': setStateFromDevtools(initialState as S) if (store === undefined) { return connection?.init(api.getState()) } return connection?.init(getTrackedConnectionState(options.name)) case 'COMMIT': if (store === undefined) { connection?.init(api.getState()) return } return connection?.init(getTrackedConnectionState(options.name)) case 'ROLLBACK': return parseJsonThen(message.state, (state) => { if (store === undefined) { setStateFromDevtools(state) connection?.init(api.getState()) return } setStateFromDevtools(state[store] as S) connection?.init(getTrackedConnectionState(options.name)) }) case 'JUMP_TO_STATE': case 'JUMP_TO_ACTION': return parseJsonThen(message.state, (state) => { if (store === undefined) { setStateFromDevtools(state) return } if ( JSON.stringify(api.getState()) !== JSON.stringify(state[store]) ) { setStateFromDevtools(state[store] as S) } }) case 'IMPORT_STATE': { const { nextLiftedState } = message.payload const lastComputedState = nextLiftedState.computedStates.slice(-1)[0]?.state if (!lastComputedState) return if (store === undefined) { setStateFromDevtools(lastComputedState) } else { setStateFromDevtools(lastComputedState[store]) } connection?.send( null as any, // FIXME no-any nextLiftedState, ) return } case 'PAUSE_RECORDING': return (isRecording = !isRecording) } return } }) return initialState } export const devtools = devtoolsImpl as unknown as Devtools const parseJsonThen = (stringified: string, fn: (parsed: T) => void) => { let parsed: T | undefined try { parsed = JSON.parse(stringified) } catch (e) { console.error( '[zustand devtools middleware] Could not parse the received json', e, ) } if (parsed !== undefined) fn(parsed as T) } ================================================ FILE: src/middleware/immer.ts ================================================ import { produce } from 'immer' import type { Draft } from 'immer' import type { StateCreator, StoreMutatorIdentifier } from '../vanilla.ts' type Immer = < T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], U = T, >( initializer: StateCreator, ) => StateCreator declare module '../vanilla' { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface StoreMutators { ['zustand/immer']: WithImmer } } type Write = Omit & U type SkipTwo = T extends { length: 0 } ? [] : T extends { length: 1 } ? [] : T extends { length: 0 | 1 } ? [] : T extends [unknown, unknown, ...infer A] ? A : T extends [unknown, unknown?, ...infer A] ? A : T extends [unknown?, unknown?, ...infer A] ? A : never type SetStateType = Exclude any> type WithImmer = Write> type StoreImmer = S extends { setState: infer SetState } ? SetState extends { (...args: infer A1): infer Sr1 (...args: infer A2): infer Sr2 } ? { // Ideally, we would want to infer the `nextStateOrUpdater` `T` type from the // `A1` type, but this is infeasible since it is an intersection with // a partial type. setState( nextStateOrUpdater: | SetStateType | Partial> | ((state: Draft>) => void), shouldReplace?: false, ...args: SkipTwo ): Sr1 setState( nextStateOrUpdater: | SetStateType | ((state: Draft>) => void), shouldReplace: true, ...args: SkipTwo ): Sr2 } : never : never type ImmerImpl = ( storeInitializer: StateCreator, ) => StateCreator const immerImpl: ImmerImpl = (initializer) => (set, get, store) => { type T = ReturnType store.setState = (updater, replace, ...args) => { const nextState = ( typeof updater === 'function' ? produce(updater as any) : updater ) as ((s: T) => T) | T | Partial return set(nextState, replace as any, ...args) } return initializer(store.setState, get, store) } export const immer = immerImpl as unknown as Immer ================================================ FILE: src/middleware/persist.ts ================================================ import type { StateCreator, StoreApi, StoreMutatorIdentifier, } from '../vanilla.ts' export interface StateStorage { getItem: (name: string) => string | null | Promise setItem: (name: string, value: string) => R removeItem: (name: string) => R } export type StorageValue = { state: S version?: number } export interface PersistStorage { getItem: ( name: string, ) => StorageValue | null | Promise | null> setItem: (name: string, value: StorageValue) => R removeItem: (name: string) => R } type JsonStorageOptions = { reviver?: (key: string, value: unknown) => unknown replacer?: (key: string, value: unknown) => unknown } export function createJSONStorage( getStorage: () => StateStorage, options?: JsonStorageOptions, ): PersistStorage | undefined { let storage: StateStorage | undefined try { storage = getStorage() } catch { // prevent error if the storage is not defined (e.g. when server side rendering a page) return } const persistStorage: PersistStorage = { getItem: (name) => { const parse = (str: string | null) => { if (str === null) { return null } return JSON.parse(str, options?.reviver) as StorageValue } const str = storage.getItem(name) ?? null if (str instanceof Promise) { return str.then(parse) } return parse(str) }, setItem: (name, newValue) => storage.setItem(name, JSON.stringify(newValue, options?.replacer)), removeItem: (name) => storage.removeItem(name), } return persistStorage } export interface PersistOptions< S, PersistedState = S, PersistReturn = unknown, > { /** Name of the storage (must be unique) */ name: string /** * Use a custom persist storage. * * Combining `createJSONStorage` helps creating a persist storage * with JSON.parse and JSON.stringify. * * @default createJSONStorage(() => window.localStorage) */ storage?: PersistStorage | undefined /** * Filter the persisted value. * * @params state The state's value */ partialize?: (state: S) => PersistedState /** * A function returning another (optional) function. * The main function will be called before the state rehydration. * The returned function will be called after the state rehydration or when an error occurred. */ onRehydrateStorage?: ( state: S, ) => ((state?: S, error?: unknown) => void) | void /** * If the stored state's version mismatch the one specified here, the storage will not be used. * This is useful when adding a breaking change to your store. */ version?: number /** * A function to perform persisted state migration. * This function will be called when persisted state versions mismatch with the one specified here. */ migrate?: ( persistedState: unknown, version: number, ) => PersistedState | Promise /** * A function to perform custom hydration merges when combining the stored state with the current one. * By default, this function does a shallow merge. */ merge?: (persistedState: unknown, currentState: S) => S /** * An optional boolean that will prevent the persist middleware from triggering hydration on initialization, * This allows you to call `rehydrate()` at a specific point in your apps rendering life-cycle. * * This is useful in SSR application. * * @default false */ skipHydration?: boolean } type PersistListener = (state: S) => void type StorePersist = S extends { getState: () => infer T setState: { // capture both overloads of setState (...args: infer Sa1): infer Sr1 (...args: infer Sa2): infer Sr2 } } ? { setState(...args: Sa1): Sr1 | Pr setState(...args: Sa2): Sr2 | Pr persist: { setOptions: (options: Partial>) => void clearStorage: () => void rehydrate: () => Promise | void hasHydrated: () => boolean onHydrate: (fn: PersistListener) => () => void onFinishHydration: (fn: PersistListener) => () => void getOptions: () => Partial> } } : never type Thenable = { then( onFulfilled: (value: Value) => V | Promise | Thenable, ): Thenable catch( onRejected: (reason: Error) => V | Promise | Thenable, ): Thenable } const toThenable = ( fn: (input: Input) => Result | Promise | Thenable, ) => (input: Input): Thenable => { try { const result = fn(input) if (result instanceof Promise) { return result as Thenable } return { then(onFulfilled) { return toThenable(onFulfilled)(result as Result) }, catch(_onRejected) { return this as Thenable }, } } catch (e: any) { return { then(_onFulfilled) { return this as Thenable }, catch(onRejected) { return toThenable(onRejected)(e) }, } } } const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => { type S = ReturnType let options = { storage: createJSONStorage(() => window.localStorage), partialize: (state: S) => state, version: 0, merge: (persistedState: unknown, currentState: S) => ({ ...currentState, ...(persistedState as object), }), ...baseOptions, } let hasHydrated = false // Counter to track hydration versions and prevent race conditions // when multiple rehydrate() calls happen concurrently let hydrationVersion = 0 const hydrationListeners = new Set>() const finishHydrationListeners = new Set>() let storage = options.storage if (!storage) { return config( (...args) => { console.warn( `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`, ) set(...(args as Parameters)) }, get, api, ) } const setItem = () => { const state = options.partialize({ ...get() }) return (storage as PersistStorage).setItem(options.name, { state, version: options.version, }) } const savedSetState = api.setState api.setState = (state, replace) => { savedSetState(state, replace as any) return setItem() } const configResult = config( (...args) => { set(...(args as Parameters)) return setItem() }, get, api, ) api.getInitialState = () => configResult // a workaround to solve the issue of not storing rehydrated state in sync storage // the set(state) value would be later overridden with initial state by create() // to avoid this, we merge the state from localStorage into the initial state. let stateFromStorage: S | undefined // rehydrate initial state with existing stored state const hydrate = () => { if (!storage) return // On the first invocation of 'hydrate', state will not yet be defined (this is // true for both the 'asynchronous' and 'synchronous' case). Pass 'configResult' // as a backup to 'get()' so listeners and 'onRehydrateStorage' are called with // the latest available state. // Increment version to invalidate any in-flight hydration const currentVersion = ++hydrationVersion hasHydrated = false hydrationListeners.forEach((cb) => cb(get() ?? configResult)) const postRehydrationCallback = options.onRehydrateStorage?.(get() ?? configResult) || undefined // bind is used to avoid `TypeError: Illegal invocation` error return toThenable(storage.getItem.bind(storage))(options.name) .then((deserializedStorageValue) => { if (deserializedStorageValue) { if ( typeof deserializedStorageValue.version === 'number' && deserializedStorageValue.version !== options.version ) { if (options.migrate) { const migration = options.migrate( deserializedStorageValue.state, deserializedStorageValue.version, ) if (migration instanceof Promise) { return migration.then((result) => [true, result] as const) } return [true, migration] as const } console.error( `State loaded from storage couldn't be migrated since no migrate function was provided`, ) } else { return [false, deserializedStorageValue.state] as const } } return [false, undefined] as const }) .then((migrationResult) => { // Abort if a newer hydration has started if (currentVersion !== hydrationVersion) { return } const [migrated, migratedState] = migrationResult stateFromStorage = options.merge( migratedState as S, get() ?? configResult, ) set(stateFromStorage as S, true) if (migrated) { return setItem() } }) .then(() => { // Abort if a newer hydration has started if (currentVersion !== hydrationVersion) { return } postRehydrationCallback?.(get(), undefined) // It's possible that 'postRehydrationCallback' updated the state. To ensure // that isn't overwritten when returning 'stateFromStorage' below // (synchronous-case only), update 'stateFromStorage' to point to the latest // state. In the asynchronous case, 'stateFromStorage' isn't used after this // callback, so there's no harm in updating it to match the latest state. stateFromStorage = get() hasHydrated = true finishHydrationListeners.forEach((cb) => cb(stateFromStorage as S)) }) .catch((e: Error) => { // Abort if a newer hydration has started if (currentVersion !== hydrationVersion) { return } postRehydrationCallback?.(undefined, e) }) } ;(api as StoreApi & StorePersist, S, unknown>).persist = { setOptions: (newOptions) => { options = { ...options, ...newOptions, } if (newOptions.storage) { storage = newOptions.storage } }, clearStorage: () => { storage?.removeItem(options.name) }, getOptions: () => options, rehydrate: () => hydrate() as Promise, hasHydrated: () => hasHydrated, onHydrate: (cb) => { hydrationListeners.add(cb) return () => { hydrationListeners.delete(cb) } }, onFinishHydration: (cb) => { finishHydrationListeners.add(cb) return () => { finishHydrationListeners.delete(cb) } }, } if (!options.skipHydration) { hydrate() } return stateFromStorage || configResult } type Persist = < T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], U = T, >( initializer: StateCreator, options: PersistOptions, ) => StateCreator declare module '../vanilla' { interface StoreMutators { 'zustand/persist': WithPersist } } type Write = Omit & U type WithPersist = Write> type PersistImpl = ( storeInitializer: StateCreator, options: PersistOptions, ) => StateCreator export const persist = persistImpl as unknown as Persist ================================================ FILE: src/middleware/redux.ts ================================================ import type { StateCreator, StoreMutatorIdentifier } from '../vanilla.ts' import type { NamedSet } from './devtools.ts' type Write = Omit & U type Action = { type: string } type StoreRedux = { dispatch: (a: A) => A dispatchFromDevtools: true } type ReduxState = { dispatch: StoreRedux['dispatch'] } type WithRedux = Write> type Redux = < T, A extends Action, Cms extends [StoreMutatorIdentifier, unknown][] = [], >( reducer: (state: T, action: A) => T, initialState: T, ) => StateCreator>, Cms, [['zustand/redux', A]]> declare module '../vanilla' { interface StoreMutators { 'zustand/redux': WithRedux } } type ReduxImpl = ( reducer: (state: T, action: A) => T, initialState: T, ) => StateCreator, [], []> const reduxImpl: ReduxImpl = (reducer, initial) => (set, _get, api) => { type S = typeof initial type A = Parameters[1] ;(api as any).dispatch = (action: A) => { ;(set as NamedSet)((state: S) => reducer(state, action), false, action) return action } ;(api as any).dispatchFromDevtools = true return { dispatch: (...args) => (api as any).dispatch(...args), ...initial } } export const redux = reduxImpl as unknown as Redux ================================================ FILE: src/middleware/ssrSafe.ts ================================================ import type { StateCreator, StoreMutatorIdentifier } from '../vanilla.ts' // This is experimental middleware. It will be changed before finalizing it. // https://github.com/pmndrs/zustand/discussions/2740 // TODO Not very happy with the middleware name. Will revisit it later. export function ssrSafe< T extends object, U extends object, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], >( config: StateCreator, isSSR: boolean = typeof window === 'undefined', ): StateCreator { return (set, get, api) => { if (!isSSR) { return config(set, get, api) } const ssrSet = () => { throw new Error('Cannot set state of Zustand store in SSR') } api.setState = ssrSet return config(ssrSet as never, get, api) } } ================================================ FILE: src/middleware/subscribeWithSelector.ts ================================================ import type { StateCreator, StoreMutatorIdentifier } from '../vanilla.ts' type SubscribeWithSelector = < T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], >( initializer: StateCreator< T, [...Mps, ['zustand/subscribeWithSelector', never]], Mcs >, ) => StateCreator type Write = Omit & U type WithSelectorSubscribe = S extends { getState: () => infer T } ? Write> : never declare module '../vanilla' { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface StoreMutators { ['zustand/subscribeWithSelector']: WithSelectorSubscribe } } type StoreSubscribeWithSelector = { subscribe: { (listener: (selectedState: T, previousSelectedState: T) => void): () => void ( selector: (state: T) => U, listener: (selectedState: U, previousSelectedState: U) => void, options?: { equalityFn?: (a: U, b: U) => boolean fireImmediately?: boolean }, ): () => void } } type SubscribeWithSelectorImpl = ( storeInitializer: StateCreator, ) => StateCreator const subscribeWithSelectorImpl: SubscribeWithSelectorImpl = (fn) => (set, get, api) => { type S = ReturnType type Listener = (state: S, previousState: S) => void const origSubscribe = api.subscribe as (listener: Listener) => () => void api.subscribe = ((selector: any, optListener: any, options: any) => { let listener: Listener = selector // if no selector if (optListener) { const equalityFn = options?.equalityFn || Object.is let currentSlice = selector(api.getState()) listener = (state) => { const nextSlice = selector(state) if (!equalityFn(currentSlice, nextSlice)) { const previousSlice = currentSlice optListener((currentSlice = nextSlice), previousSlice) } } if (options?.fireImmediately) { optListener(currentSlice, currentSlice) } } return origSubscribe(listener) }) as any const initialState = fn(set, get, api) return initialState } export const subscribeWithSelector = subscribeWithSelectorImpl as unknown as SubscribeWithSelector ================================================ FILE: src/middleware.ts ================================================ export { redux } from './middleware/redux.ts' export { devtools, type DevtoolsOptions, type NamedSet, } from './middleware/devtools.ts' export { subscribeWithSelector } from './middleware/subscribeWithSelector.ts' export { combine } from './middleware/combine.ts' export { persist, createJSONStorage, type StateStorage, type StorageValue, type PersistStorage, type PersistOptions, } from './middleware/persist.ts' export { ssrSafe as unstable_ssrSafe } from './middleware/ssrSafe.ts' ================================================ FILE: src/react/shallow.ts ================================================ import React from 'react' import { shallow } from '../vanilla/shallow.ts' export function useShallow(selector: (state: S) => U): (state: S) => U { const prev = React.useRef(undefined) return (state) => { const next = selector(state) return shallow(prev.current, next) ? (prev.current as U) : (prev.current = next) } } ================================================ FILE: src/react.ts ================================================ import React from 'react' import { createStore } from './vanilla.ts' import type { ExtractState, Mutate, StateCreator, StoreApi, StoreMutatorIdentifier, } from './vanilla.ts' type ReadonlyStoreApi = Pick< StoreApi, 'getState' | 'getInitialState' | 'subscribe' > const identity = (arg: T): T => arg export function useStore>( api: S, ): ExtractState export function useStore, U>( api: S, selector: (state: ExtractState) => U, ): U export function useStore( api: ReadonlyStoreApi, selector: (state: TState) => StateSlice = identity as any, ) { const slice = React.useSyncExternalStore( api.subscribe, React.useCallback(() => selector(api.getState()), [api, selector]), React.useCallback(() => selector(api.getInitialState()), [api, selector]), ) React.useDebugValue(slice) return slice } export type UseBoundStore> = { (): ExtractState (selector: (state: ExtractState) => U): U } & S type Create = { ( initializer: StateCreator, ): UseBoundStore, Mos>> (): ( initializer: StateCreator, ) => UseBoundStore, Mos>> } const createImpl = (createState: StateCreator) => { const api = createStore(createState) const useBoundStore: any = (selector?: any) => useStore(api, selector) Object.assign(useBoundStore, api) return useBoundStore } export const create = ((createState: StateCreator | undefined) => createState ? createImpl(createState) : createImpl) as Create ================================================ FILE: src/shallow.ts ================================================ export { shallow } from './vanilla/shallow.ts' export { useShallow } from './react/shallow.ts' ================================================ FILE: src/traditional.ts ================================================ import React from 'react' import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector' import { createStore } from './vanilla.ts' import type { ExtractState, Mutate, StateCreator, StoreApi, StoreMutatorIdentifier, } from './vanilla.ts' const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports type ReadonlyStoreApi = Pick< StoreApi, 'getState' | 'getInitialState' | 'subscribe' > const identity = (arg: T): T => arg export function useStoreWithEqualityFn>( api: S, ): ExtractState export function useStoreWithEqualityFn, U>( api: S, selector: (state: ExtractState) => U, equalityFn?: (a: U, b: U) => boolean, ): U export function useStoreWithEqualityFn( api: ReadonlyStoreApi, selector: (state: TState) => StateSlice = identity as any, equalityFn?: (a: StateSlice, b: StateSlice) => boolean, ) { const slice = useSyncExternalStoreWithSelector( api.subscribe, api.getState, api.getInitialState, selector, equalityFn, ) React.useDebugValue(slice) return slice } export type UseBoundStoreWithEqualityFn> = { (): ExtractState ( selector: (state: ExtractState) => U, equalityFn?: (a: U, b: U) => boolean, ): U } & S type CreateWithEqualityFn = { ( initializer: StateCreator, defaultEqualityFn?: (a: U, b: U) => boolean, ): UseBoundStoreWithEqualityFn, Mos>> (): ( initializer: StateCreator, defaultEqualityFn?: (a: U, b: U) => boolean, ) => UseBoundStoreWithEqualityFn, Mos>> } const createWithEqualityFnImpl = ( createState: StateCreator, defaultEqualityFn?: (a: U, b: U) => boolean, ) => { const api = createStore(createState) const useBoundStoreWithEqualityFn: any = ( selector?: any, equalityFn = defaultEqualityFn, ) => useStoreWithEqualityFn(api, selector, equalityFn) Object.assign(useBoundStoreWithEqualityFn, api) return useBoundStoreWithEqualityFn } export const createWithEqualityFn = (( createState: StateCreator | undefined, defaultEqualityFn?: (a: U, b: U) => boolean, ) => createState ? createWithEqualityFnImpl(createState, defaultEqualityFn) : createWithEqualityFnImpl) as CreateWithEqualityFn ================================================ FILE: src/types.d.ts ================================================ declare interface ImportMeta { env?: { MODE: string } } ================================================ FILE: src/vanilla/shallow.ts ================================================ const isIterable = (obj: object): obj is Iterable => Symbol.iterator in obj const hasIterableEntries = ( value: Iterable, ): value is Iterable & { entries(): Iterable<[unknown, unknown]> } => // HACK: avoid checking entries type 'entries' in value const compareEntries = ( valueA: { entries(): Iterable<[unknown, unknown]> }, valueB: { entries(): Iterable<[unknown, unknown]> }, ) => { const mapA = valueA instanceof Map ? valueA : new Map(valueA.entries()) const mapB = valueB instanceof Map ? valueB : new Map(valueB.entries()) if (mapA.size !== mapB.size) { return false } for (const [key, value] of mapA) { if (!mapB.has(key) || !Object.is(value, mapB.get(key))) { return false } } return true } // Ordered iterables const compareIterables = ( valueA: Iterable, valueB: Iterable, ) => { const iteratorA = valueA[Symbol.iterator]() const iteratorB = valueB[Symbol.iterator]() let nextA = iteratorA.next() let nextB = iteratorB.next() while (!nextA.done && !nextB.done) { if (!Object.is(nextA.value, nextB.value)) { return false } nextA = iteratorA.next() nextB = iteratorB.next() } return !!nextA.done && !!nextB.done } export function shallow(valueA: T, valueB: T): boolean { if (Object.is(valueA, valueB)) { return true } if ( typeof valueA !== 'object' || valueA === null || typeof valueB !== 'object' || valueB === null ) { return false } if (Object.getPrototypeOf(valueA) !== Object.getPrototypeOf(valueB)) { return false } if (isIterable(valueA) && isIterable(valueB)) { if (hasIterableEntries(valueA) && hasIterableEntries(valueB)) { return compareEntries(valueA, valueB) } return compareIterables(valueA, valueB) } // assume plain objects return compareEntries( { entries: () => Object.entries(valueA) }, { entries: () => Object.entries(valueB) }, ) } ================================================ FILE: src/vanilla.ts ================================================ type SetStateInternal = { _( partial: T | Partial | { _(state: T): T | Partial }['_'], replace?: false, ): void _(state: T | { _(state: T): T }['_'], replace: true): void }['_'] export interface StoreApi { setState: SetStateInternal getState: () => T getInitialState: () => T subscribe: (listener: (state: T, prevState: T) => void) => () => void } export type ExtractState = S extends { getState: () => infer T } ? T : never type Get = K extends keyof T ? T[K] : F export type Mutate = number extends Ms['length' & keyof Ms] ? S : Ms extends [] ? S : Ms extends [[infer Mi, infer Ma], ...infer Mrs] ? Mutate[Mi & StoreMutatorIdentifier], Mrs> : never export type StateCreator< T, Mis extends [StoreMutatorIdentifier, unknown][] = [], Mos extends [StoreMutatorIdentifier, unknown][] = [], U = T, > = (( setState: Get, Mis>, 'setState', never>, getState: Get, Mis>, 'getState', never>, store: Mutate, Mis>, ) => U) & { $$storeMutators?: Mos } // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-object-type export interface StoreMutators {} export type StoreMutatorIdentifier = keyof StoreMutators type CreateStore = { ( initializer: StateCreator, ): Mutate, Mos> (): ( initializer: StateCreator, ) => Mutate, Mos> } type CreateStoreImpl = < T, Mos extends [StoreMutatorIdentifier, unknown][] = [], >( initializer: StateCreator, ) => Mutate, Mos> const createStoreImpl: CreateStoreImpl = (createState) => { type TState = ReturnType type Listener = (state: TState, prevState: TState) => void let state: TState const listeners: Set = new Set() const setState: StoreApi['setState'] = (partial, replace) => { // TODO: Remove type assertion once https://github.com/microsoft/TypeScript/issues/37663 is resolved // https://github.com/microsoft/TypeScript/issues/37663#issuecomment-759728342 const nextState = typeof partial === 'function' ? (partial as (state: TState) => TState)(state) : partial if (!Object.is(nextState, state)) { const previousState = state state = (replace ?? (typeof nextState !== 'object' || nextState === null)) ? (nextState as TState) : Object.assign({}, state, nextState) listeners.forEach((listener) => listener(state, previousState)) } } const getState: StoreApi['getState'] = () => state const getInitialState: StoreApi['getInitialState'] = () => initialState const subscribe: StoreApi['subscribe'] = (listener) => { listeners.add(listener) // Unsubscribe return () => listeners.delete(listener) } const api = { setState, getState, getInitialState, subscribe } const initialState = (state = createState(setState, getState, api)) return api as any } export const createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore ================================================ FILE: tests/basic.test.tsx ================================================ import { Component as ClassComponent, StrictMode, useEffect, useLayoutEffect, useState, } from 'react' import type { ReactNode } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import ReactDOM from 'react-dom' import { afterEach, expect, it, vi } from 'vitest' import { create } from 'zustand' import type { StoreApi } from 'zustand' import { createWithEqualityFn } from 'zustand/traditional' const consoleError = console.error afterEach(() => { console.error = consoleError }) it('creates a store hook and api object', () => { let params const result = create((...args) => { params = args return { value: null } }) expect({ params, result }).toMatchInlineSnapshot(` { "params": [ [Function], [Function], { "getInitialState": [Function], "getState": [Function], "setState": [Function], "subscribe": [Function], }, ], "result": [Function], } `) }) type CounterState = { count: number inc: () => void } it('uses the store with no args', () => { const useBoundStore = create((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) function Counter() { const { count, inc } = useBoundStore() useEffect(inc, [inc]) return
count: {count}
} render( <> , ) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('uses the store with selectors', () => { const useBoundStore = create((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) function Counter() { const count = useBoundStore((s) => s.count) const inc = useBoundStore((s) => s.inc) useEffect(inc, [inc]) return
count: {count}
} render( <> , ) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('uses the store with a selector and equality checker', () => { const useBoundStore = createWithEqualityFn( () => ({ item: { value: 0 } }), Object.is, ) const { setState } = useBoundStore let renderCount = 0 function Component() { // Prevent re-render if new value === 1. const item = useBoundStore( (s) => s.item, (_, newItem) => newItem.value === 1, ) return (
renderCount: {++renderCount}, value: {item.value}
) } render( <> , ) expect(screen.getByText('renderCount: 1, value: 0')).toBeInTheDocument() // This will not cause a re-render. act(() => setState({ item: { value: 1 } })) expect(screen.getByText('renderCount: 1, value: 0')).toBeInTheDocument() // This will cause a re-render. act(() => setState({ item: { value: 2 } })) expect(screen.getByText('renderCount: 2, value: 2')).toBeInTheDocument() }) it('only re-renders if selected state has changed', () => { const useBoundStore = create((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) let counterRenderCount = 0 let controlRenderCount = 0 function Counter() { const count = useBoundStore((state) => state.count) counterRenderCount++ return
count: {count}
} function Control() { const inc = useBoundStore((state) => state.inc) controlRenderCount++ return } render( <> , ) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(counterRenderCount).toBe(2) expect(controlRenderCount).toBe(1) }) it('can batch updates', () => { const useBoundStore = create((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) function Counter() { const { count, inc } = useBoundStore() useEffect(() => { ReactDOM.unstable_batchedUpdates(() => { inc() inc() }) }, [inc]) return
count: {count}
} render( <> , ) expect(screen.getByText('count: 2')).toBeInTheDocument() }) it('can update the selector', () => { type State = { one: string; two: string } type Props = { selector: (state: State) => string } const useBoundStore = create(() => ({ one: 'one', two: 'two', })) function Component({ selector }: Props) { return
{useBoundStore(selector)}
} const { rerender } = render( s.one} /> , ) expect(screen.getByText('one')).toBeInTheDocument() rerender( s.two} /> , ) expect(screen.getByText('two')).toBeInTheDocument() }) it('can update the equality checker', () => { type State = { value: number } type Props = { equalityFn: (a: State, b: State) => boolean } const useBoundStore = createWithEqualityFn( () => ({ value: 0 }), Object.is, ) const { setState } = useBoundStore const selector = (s: State) => s let renderCount = 0 function Component({ equalityFn }: Props) { const { value } = useBoundStore(selector, equalityFn) return (
renderCount: {++renderCount}, value: {value}
) } // Set an equality checker that always returns false to always re-render. const { rerender } = render( <> false} /> , ) // This will cause a re-render due to the equality checker. act(() => setState({ value: 0 })) expect(screen.getByText('renderCount: 2, value: 0')).toBeInTheDocument() // Set an equality checker that always returns true to never re-render. rerender( <> true} /> , ) // This will NOT cause a re-render due to the equality checker. act(() => setState({ value: 1 })) expect(screen.getByText('renderCount: 3, value: 0')).toBeInTheDocument() }) it('can call useBoundStore with progressively more arguments', () => { type State = { value: number } type Props = { selector?: (state: State) => number equalityFn?: (a: number, b: number) => boolean } const useBoundStore = createWithEqualityFn( () => ({ value: 0 }), Object.is, ) const { setState } = useBoundStore let renderCount = 0 function Component({ selector, equalityFn }: Props) { const value = useBoundStore(selector as any, equalityFn) return (
renderCount: {++renderCount}, value: {JSON.stringify(value)}
) } // Render with no args. const { rerender } = render( <> , ) expect( screen.getByText('renderCount: 1, value: {"value":0}'), ).toBeInTheDocument() // Render with selector. rerender( <> s.value} /> , ) expect(screen.getByText('renderCount: 2, value: 0')).toBeInTheDocument() // Render with selector and equality checker. rerender( <> s.value} equalityFn={(oldV, newV) => oldV > newV} /> , ) // Should not cause a re-render because new value is less than previous. act(() => setState({ value: -1 })) expect(screen.getByText('renderCount: 3, value: 0')).toBeInTheDocument() act(() => setState({ value: 1 })) expect(screen.getByText('renderCount: 4, value: 1')).toBeInTheDocument() }) it('can throw an error in selector', () => { console.error = vi.fn() type State = { value: string | number } const initialState: State = { value: 'foo' } const useBoundStore = create(() => initialState) const { setState } = useBoundStore const selector = (s: State) => // @ts-expect-error This function is supposed to throw an error s.value.toUpperCase() class ErrorBoundary extends ClassComponent< { children?: ReactNode | undefined }, { hasError: boolean } > { constructor(props: { children?: ReactNode | undefined }) { super(props) this.state = { hasError: false } } static getDerivedStateFromError() { return { hasError: true } } render() { // eslint-disable-next-line testing-library/no-node-access return this.state.hasError ?
errored
: this.props.children } } function Component() { useBoundStore(selector) return
no error
} render( , ) expect(screen.getByText('no error')).toBeInTheDocument() act(() => { setState({ value: 123 }) }) expect(screen.getByText('errored')).toBeInTheDocument() }) it('can throw an error in equality checker', () => { console.error = vi.fn() type State = { value: string | number } const initialState: State = { value: 'foo' } const useBoundStore = createWithEqualityFn(() => initialState, Object.is) const { setState } = useBoundStore const selector = (s: State) => s const equalityFn = (a: State, b: State) => // @ts-expect-error This function is supposed to throw an error a.value.trim() === b.value.trim() class ErrorBoundary extends ClassComponent< { children?: ReactNode | undefined }, { hasError: boolean } > { constructor(props: { children?: ReactNode | undefined }) { super(props) this.state = { hasError: false } } static getDerivedStateFromError() { return { hasError: true } } render() { // eslint-disable-next-line testing-library/no-node-access return this.state.hasError ?
errored
: this.props.children } } function Component() { useBoundStore(selector, equalityFn) return
no error
} render( , ) expect(screen.getByText('no error')).toBeInTheDocument() act(() => { setState({ value: 123 }) }) expect(screen.getByText('errored')).toBeInTheDocument() }) it('can get the store', () => { type State = { value: number getState1: () => State getState2: () => State } const { getState } = create((_, get) => ({ value: 1, getState1: () => get(), getState2: (): State => getState(), })) expect(getState().getState1().value).toBe(1) expect(getState().getState2().value).toBe(1) }) it('can set the store', () => { type State = { value: number setState1: StoreApi['setState'] setState2: StoreApi['setState'] } const { setState, getState } = create((set) => ({ value: 1, setState1: (v) => set(v), setState2: (v) => setState(v), })) getState().setState1({ value: 2 }) expect(getState().value).toBe(2) getState().setState2({ value: 3 }) expect(getState().value).toBe(3) getState().setState1((s) => ({ value: ++s.value })) expect(getState().value).toBe(4) getState().setState2((s) => ({ value: ++s.value })) expect(getState().value).toBe(5) }) it('both NaN should not update', () => { const { setState, subscribe } = create(() => NaN) const fn = vi.fn() subscribe(fn) setState(NaN) expect(fn).not.toBeCalled() }) it('can set the store without merging', () => { const { setState, getState } = create<{ a: number } | { b: number }>( (_set) => ({ a: 1, }), ) // Should override the state instead of merging. setState({ b: 2 }, true) expect(getState()).toEqual({ b: 2 }) }) it('only calls selectors when necessary with static selector', () => { type State = { a: number; b: number } const useBoundStore = createWithEqualityFn(() => ({ a: 0, b: 0 })) const { setState } = useBoundStore let staticSelectorCallCount = 0 function staticSelector(s: State) { staticSelectorCallCount++ return s.a } function Component() { useBoundStore(staticSelector) return ( <>
static: {staticSelectorCallCount}
) } const { rerender } = render( <> , ) expect(screen.getByText('static: 1')).toBeInTheDocument() rerender( <> , ) expect(screen.getByText('static: 1')).toBeInTheDocument() act(() => setState({ a: 1, b: 1 })) expect(screen.getByText('static: 2')).toBeInTheDocument() }) it('only calls selectors when necessary (traditional)', () => { type State = { a: number; b: number } const useBoundStore = createWithEqualityFn(() => ({ a: 0, b: 0 })) const { setState } = useBoundStore let inlineSelectorCallCount = 0 let staticSelectorCallCount = 0 function staticSelector(s: State) { staticSelectorCallCount++ return s.a } function Component() { useBoundStore((s) => (inlineSelectorCallCount++, s.b)) useBoundStore(staticSelector) return ( <>
inline: {inlineSelectorCallCount}
static: {staticSelectorCallCount}
) } const { rerender } = render( <> , ) expect(screen.getByText('inline: 1')).toBeInTheDocument() expect(screen.getByText('static: 1')).toBeInTheDocument() rerender( <> , ) expect(screen.getByText('inline: 2')).toBeInTheDocument() expect(screen.getByText('static: 1')).toBeInTheDocument() act(() => setState({ a: 1, b: 1 })) expect(screen.getByText('inline: 4')).toBeInTheDocument() expect(screen.getByText('static: 2')).toBeInTheDocument() }) it('ensures parent components subscribe before children', () => { type State = { childItems: { [key: string]: { text: string } } } type Props = { id: string } const useBoundStore = create(() => ({ childItems: { '1': { text: 'child 1' }, '2': { text: 'child 2' }, }, })) const api = useBoundStore function changeState() { api.setState({ childItems: { '3': { text: 'child 3' }, }, }) } function Child({ id }: Props) { const text = useBoundStore((s) => s.childItems[id]?.text) return
{text}
} function Parent() { const childStates = useBoundStore((s) => s.childItems) return ( <> {Object.keys(childStates).map((id) => ( ))} ) } render( , ) fireEvent.click(screen.getByText('change state')) expect(screen.getByText('child 3')).toBeInTheDocument() }) // https://github.com/pmndrs/zustand/issues/84 it('ensures the correct subscriber is removed on unmount', () => { const useBoundStore = create(() => ({ count: 0 })) const api = useBoundStore function increment() { api.setState(({ count }) => ({ count: count + 1 })) } function Count() { const c = useBoundStore((s) => s.count) return
count: {c}
} function CountWithInitialIncrement() { useLayoutEffect(increment, []) return } function Component() { const [Counter, setCounter] = useState(() => CountWithInitialIncrement) useLayoutEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect setCounter(() => Count) }, []) return ( <> ) } render( <> , ) expect(screen.getAllByText('count: 1').length).toBe(2) act(increment) expect(screen.getAllByText('count: 2').length).toBe(2) }) // https://github.com/pmndrs/zustand/issues/86 it('ensures a subscriber is not mistakenly overwritten', () => { const useBoundStore = create(() => ({ count: 0 })) const { setState } = useBoundStore function Count1() { const c = useBoundStore((s) => s.count) return
count1: {c}
} function Count2() { const c = useBoundStore((s) => s.count) return
count2: {c}
} // Add 1st subscriber. const { rerender } = render( , ) // Replace 1st subscriber with another. rerender( , ) // Add 2 additional subscribers. rerender( , ) // Call all subscribers act(() => setState({ count: 1 })) expect(screen.getAllByText('count1: 1').length).toBe(2) expect(screen.getAllByText('count2: 1').length).toBe(1) }) it('works with non-object state', () => { const useCount = create(() => 1) const inc = () => useCount.setState((c) => c + 1) const Counter = () => { const count = useCount() return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() }) it('works with "undefined" state', () => { const useUndefined = create(() => undefined) const Component = () => { const str = useUndefined((v) => v || 'undefined') return
str: {str}
} render( , ) expect(screen.getByText('str: undefined')).toBeInTheDocument() }) ================================================ FILE: tests/devtools.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { Mock } from 'vitest' import { devtools, redux } from 'zustand/middleware' import { createStore } from 'zustand/vanilla' import type { StoreApi } from 'zustand/vanilla' type TupleOfEqualLengthH< Arr extends unknown[], T, Acc extends T[], > = Arr extends [unknown, ...infer Rest] ? TupleOfEqualLengthH : Acc type TupleOfEqualLength = number extends Arr['length'] ? T[] : TupleOfEqualLengthH type Connection = { subscribers: ((message: unknown) => void)[] api: { subscribe: Mock<(f: (message: unknown) => void) => () => void> unsubscribe: Mock send: Mock init: Mock error: Mock dispatch?: Mock } } const namedConnections = new Map() const unnamedConnections = new Map() function assertAllAreDefined(arr: (T | undefined)[]): asserts arr is T[] { if (arr.some((e) => e === undefined)) { throw new Error() } } function getNamedConnectionApis( ...keys: Keys ) { const apis = keys.map((k) => namedConnections.get(k)?.api) assertAllAreDefined(apis) return apis as TupleOfEqualLength } function getNamedConnectionSubscribers( ...keys: Keys ) { const subscribers = keys.map((k) => { const subs = namedConnections.get(k)?.subscribers if (subs?.length !== 1) throw new Error() return subs[0] }) assertAllAreDefined(subscribers) return subscribers as TupleOfEqualLength< Keys, Connection['subscribers'][number] > } function getUnnamedConnectionApis(...keys: Keys) { const apis = keys.map((k) => unnamedConnections.get(k)?.api) assertAllAreDefined(apis) return apis as TupleOfEqualLength } function getUnnamedConnectionSubscribers(...keys: Keys) { const subscribers = keys.map((k) => { const subs = unnamedConnections.get(k)?.subscribers if (!subs) { throw new Error() } return subs[0] }) assertAllAreDefined(subscribers) return subscribers as TupleOfEqualLength< Keys, Connection['subscribers'][number] > } function getKeyFromOptions(options: any): string | undefined { let key: string | undefined = options?.name if (options?.testStore) { key = `${options?.name}|${options?.testStore}` } return key } const extensionConnector = { connect: vi.fn((options: any) => { const key = getKeyFromOptions(options) //console.log('options', options) const areNameUndefinedMapsNeeded = options.testConnectionId !== undefined && options?.name === undefined const connectionMap = areNameUndefinedMapsNeeded ? unnamedConnections : namedConnections const subscribers: Connection['subscribers'] = [] const api: Connection['api'] = { subscribe: vi.fn((f: (m: unknown) => void) => { subscribers.push(f) return () => {} }), unsubscribe: vi.fn(() => { connectionMap.delete( areNameUndefinedMapsNeeded ? options.testConnectionId : key, ) }), send: vi.fn(), init: vi.fn(), error: vi.fn(), } connectionMap.set( areNameUndefinedMapsNeeded ? options.testConnectionId : key, { subscribers, api, }, ) return api }), } ;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = extensionConnector beforeEach(() => { vi.resetModules() extensionConnector.connect.mockClear() namedConnections.clear() unnamedConnections.clear() }) it('connects to the extension by passing the options and initializes', async () => { const options = { name: 'test', foo: 'bar' } const initialState = { count: 0 } createStore(devtools(() => initialState, { enabled: true, ...options })) expect(extensionConnector.connect).toHaveBeenLastCalledWith(options) const [conn] = getNamedConnectionApis(options.name) expect(conn.init).toHaveBeenLastCalledWith(initialState) }) describe('If there is no extension installed...', () => { let savedConsoleWarn: any beforeEach(() => { savedConsoleWarn = console.warn console.warn = vi.fn() ;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = undefined }) afterEach(() => { console.warn = savedConsoleWarn ;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = extensionConnector }) it('does not throw', async () => { expect(() => { createStore(devtools(() => ({ count: 0 }))) }).not.toThrow() }) it('does not warn', async () => { createStore(devtools(() => ({ count: 0 }))) expect(console.warn).not.toBeCalled() }) }) describe('When state changes...', () => { it("sends { type: setStateName || 'anonymous`, ...rest } as the action with current state", async () => { const options = { name: 'testOptionsName', enabled: true, } const api = createStore(devtools(() => ({ count: 0, foo: 'bar' }), options)) api.setState({ count: 10 }, false, 'testSetStateName') const [connection] = getNamedConnectionApis(options.name) expect(connection.send).toHaveBeenLastCalledWith( { type: 'testSetStateName' }, { count: 10, foo: 'bar' }, ) api.setState({ count: 15 }, false, { type: 'testSetStateName', payload: 15, }) expect(connection.send).toHaveBeenLastCalledWith( { type: 'testSetStateName', payload: 15 }, { count: 15, foo: 'bar' }, ) api.setState({ count: 5, foo: 'baz' }, true) expect(connection.send).toHaveBeenLastCalledWith( { type: 'anonymous' }, { count: 5, foo: 'baz' }, ) }) }) describe('When state changes with automatic setter inferring...', () => { it("sends { type: setStateName || 'setCount`, ...rest } as the action with current state", async () => { const options = { name: 'testOptionsName', enabled: true, } const api = createStore<{ count: number setCount: (count: number) => void }>()( devtools( (set) => ({ count: 0, setCount: (newCount: number) => { set({ count: newCount }) }, }), options, ), ) api.getState().setCount(10) const [connection] = getNamedConnectionApis(options.name) expect(connection.send).toHaveBeenLastCalledWith( { type: expect.stringMatching(/^(Object\.setCount|anonymous)$/) }, { count: 10, setCount: expect.any(Function) }, ) }) }) describe('when it receives a message of type...', () => { describe('ACTION...', () => { it('does nothing', async () => { const initialState = { count: 0 } const api = createStore(devtools(() => initialState, { enabled: true })) const setState = vi.spyOn(api, 'setState') const [subscriber] = getNamedConnectionSubscribers(undefined) subscriber({ type: 'ACTION', payload: '{ "type": "INCREMENT" }', }) expect(api.getState()).toBe(initialState) expect(setState).not.toBeCalled() }) it('unless action type is __setState', async () => { const initialState = { count: 0 } const api = createStore(devtools(() => initialState, { enabled: true })) const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'ACTION', payload: '{ "type": "__setState", "state": { "foo": "bar" } }', }) expect(api.getState()).toStrictEqual({ ...initialState, foo: 'bar' }) }) it('does nothing even if there is `api.dispatch`', async () => { const initialState = { count: 0 } const api = createStore(devtools(() => initialState, { enabled: true })) ;(api as any).dispatch = vi.fn() const setState = vi.spyOn(api, 'setState') const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'ACTION', payload: '{ "type": "INCREMENT" }', }) expect(api.getState()).toBe(initialState) expect(setState).not.toBeCalled() expect((api as any).dispatch).not.toBeCalled() }) it('dispatches with `api.dispatch` when `api.dispatchFromDevtools` is set to true', async () => { const initialState = { count: 0 } const api = createStore(devtools(() => initialState, { enabled: true })) ;(api as any).dispatch = vi.fn() ;(api as any).dispatchFromDevtools = true const setState = vi.spyOn(api, 'setState') const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'ACTION', payload: '{ "type": "INCREMENT" }', }) expect(api.getState()).toBe(initialState) expect(setState).not.toBeCalled() expect((api as any).dispatch).toHaveBeenLastCalledWith({ type: 'INCREMENT', }) }) it('does not throw for unsupported payload', async () => { const initialState = { count: 0 } const api = createStore(devtools(() => initialState, { enabled: true })) ;(api as any).dispatch = vi.fn() ;(api as any).dispatchFromDevtools = true const setState = vi.spyOn(api, 'setState') const originalConsoleError = console.error console.error = vi.fn() const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) expect(() => { connectionSubscriber({ type: 'ACTION', payload: 'this.increment()', }) }).not.toThrow() expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('this.increment()') } catch (e) { return e } })(), ) expect(() => { connectionSubscriber({ type: 'ACTION', payload: { name: 'increment', args: [] }, }) }).not.toThrow() expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Unsupported action format', ) expect(api.getState()).toBe(initialState) expect(setState).not.toBeCalled() expect((api as any).dispatch).not.toBeCalled() console.error = originalConsoleError }) }) describe('DISPATCH and payload of type...', () => { it('RESET, it inits with initial state', async () => { const initialState = { count: 0 } const api = createStore(devtools(() => initialState, { enabled: true })) api.setState({ count: 1 }) const [connection] = getNamedConnectionApis(undefined) connection.send.mockClear() const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'RESET' }, }) expect(api.getState()).toStrictEqual(initialState) expect(connection.init).toHaveBeenLastCalledWith(initialState) expect(connection.send).not.toBeCalled() }) it('COMMIT, it inits with current state', async () => { const initialState = { count: 0 } const api = createStore(devtools(() => initialState, { enabled: true })) api.setState({ count: 2 }) const currentState = api.getState() const [connection] = getNamedConnectionApis(undefined) connection.send.mockClear() const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'COMMIT' }, }) expect(connection.init).toHaveBeenLastCalledWith(currentState) expect(connection.send).not.toBeCalled() }) describe('ROLLBACK...', () => { it('updates state without recording and inits with `message.state`', async () => { const initialState = { count: 0, increment: () => {} } const api = createStore(devtools(() => initialState, { enabled: true })) const newState = { foo: 'bar' } const [connection] = getNamedConnectionApis(undefined) connection.send.mockClear() const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'ROLLBACK' }, state: JSON.stringify(newState), }) expect(api.getState()).toStrictEqual({ ...initialState, ...newState }) expect(connection.init).toHaveBeenLastCalledWith({ ...initialState, ...newState, }) expect(connection.send).not.toBeCalled() }) it('does not throw for unparsable `message.state`', async () => { const increment = () => {} const initialState = { count: 0, increment } const api = createStore(devtools(() => initialState, { enabled: true })) const originalConsoleError = console.error console.error = vi.fn() const [connection] = getNamedConnectionApis(undefined) connection.init.mockClear() connection.send.mockClear() const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'ROLLBACK' }, state: 'foobar', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar') } catch (e) { return e } })(), ) expect(api.getState()).toBe(initialState) expect(connection.init).not.toBeCalled() expect(connection.send).not.toBeCalled() console.error = originalConsoleError }) }) describe('JUMP_TO_STATE...', () => { const increment = () => {} it('updates state without recording with `message.state`', async () => { const initialState = { count: 0, increment } const api = createStore(devtools(() => initialState, { enabled: true })) const newState = { foo: 'bar' } const [connection] = getNamedConnectionApis(undefined) connection.send.mockClear() const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE' }, state: JSON.stringify(newState), }) expect(api.getState()).toStrictEqual({ ...initialState, ...newState }) expect(connection.send).not.toBeCalled() }) it('does not throw for unparsable `message.state`', async () => { const initialState = { count: 0, increment: () => {} } const api = createStore(devtools(() => initialState, { enabled: true })) const originalConsoleError = console.error console.error = vi.fn() const [connection] = getNamedConnectionApis(undefined) connection.send.mockClear() const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE' }, state: 'foobar', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar') } catch (e) { return e } })(), ) expect(api.getState()).toBe(initialState) expect(connection.send).not.toBeCalled() console.error = originalConsoleError }) }) describe('JUMP_TO_ACTION...', () => { it('updates state without recording with `message.state`', async () => { const initialState = { count: 0, increment: () => {} } const api = createStore(devtools(() => initialState, { enabled: true })) const newState = { foo: 'bar' } const [connection] = getNamedConnectionApis(undefined) connection.send.mockClear() const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'JUMP_TO_ACTION' }, state: JSON.stringify(newState), }) expect(api.getState()).toStrictEqual({ ...initialState, ...newState }) expect(connection.send).not.toBeCalled() }) it('does not throw for unparsable `message.state`', async () => { const increment = () => {} const initialState = { count: 0, increment } const api = createStore(devtools(() => initialState, { enabled: true })) const originalConsoleError = console.error console.error = vi.fn() const [connection] = getNamedConnectionApis(undefined) connection.send.mockClear() const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'JUMP_TO_ACTION' }, state: 'foobar', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar') } catch (e) { return e } })(), ) expect(api.getState()).toBe(initialState) expect(connection.send).not.toBeCalled() console.error = originalConsoleError }) }) it('IMPORT_STATE, it updates state without recording and inits the last computedState', async () => { const initialState = { count: 0, increment: () => {} } const api = createStore(devtools(() => initialState, { enabled: true })) const nextLiftedState = { computedStates: [{ state: { count: 4 } }, { state: { count: 5 } }], } const [connection] = getNamedConnectionApis(undefined) connection.send.mockClear() const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'IMPORT_STATE', nextLiftedState, }, }) expect(api.getState()).toStrictEqual({ ...initialState, ...nextLiftedState.computedStates.slice(-1)[0]?.state, }) expect(connection.send).toHaveBeenLastCalledWith(null, nextLiftedState) }) it('PAUSE_RECORDING, it toggles the sending of actions', async () => { const api = createStore(devtools(() => ({ count: 0 }), { enabled: true })) api.setState({ count: 1 }, false, 'increment') const [connection] = getNamedConnectionApis(undefined) const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) expect(connection.send).toHaveBeenLastCalledWith( { type: 'increment' }, { count: 1 }, ) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'PAUSE_RECORDING' }, }) api.setState({ count: 2 }, false, 'increment') expect(connection.send).toHaveBeenLastCalledWith( { type: 'increment' }, { count: 1 }, ) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'PAUSE_RECORDING' }, }) api.setState({ count: 3 }, false, 'increment') expect(connection.send).toHaveBeenLastCalledWith( { type: 'increment' }, { count: 3 }, ) }) }) }) describe('with redux middleware', () => { let api: StoreApi<{ count: number dispatch: ( action: { type: 'INCREMENT' } | { type: 'DECREMENT' }, ) => { type: 'INCREMENT' } | { type: 'DECREMENT' } }> it('works as expected', async () => { api = createStore( devtools( redux( ( { count }, { type }: { type: 'INCREMENT' } | { type: 'DECREMENT' }, ) => ({ count: count + (type === 'INCREMENT' ? 1 : -1), }), { count: 0 }, ), { enabled: true }, ), ) ;(api as any).dispatch({ type: 'INCREMENT' }) ;(api as any).dispatch({ type: 'INCREMENT' }) const [connection] = getNamedConnectionApis(undefined) const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'ACTION', payload: JSON.stringify({ type: 'DECREMENT' }), }) expect(connection.init.mock.calls).toMatchObject([ [{ count: 0 }] as unknown as Record, ]) expect(connection.send.mock.calls).toMatchObject([ [{ type: 'INCREMENT' }, { count: 1 }] as unknown as Record< string, unknown >, [{ type: 'INCREMENT' }, { count: 2 }] as unknown as Record< string, unknown >, [{ type: 'DECREMENT' }, { count: 1 }] as unknown as Record< string, unknown >, ]) expect(api.getState()).toMatchObject({ count: 1 }) }) it('[DEV-ONLY] warns about misusage', () => { const originalConsoleWarn = console.warn console.warn = vi.fn() ;(api as any).dispatch({ type: '__setState' as any }) expect(console.warn).toHaveBeenLastCalledWith( '[zustand devtools middleware] "__setState" action type is reserved ' + 'to set state from the devtools. Avoid using it.', ) console.warn = originalConsoleWarn }) }) describe('different envs', () => { let savedConsoleWarn: any beforeEach(() => { savedConsoleWarn = console.warn console.warn = vi.fn() }) afterEach(() => { console.warn = savedConsoleWarn }) it('works in non-browser env', async () => { const originalWindow = globalThis.window globalThis.window = undefined as any expect(() => { createStore(devtools(() => ({ count: 0 }), { enabled: true })) }).not.toThrow() globalThis.window = originalWindow }) it('works in react native env', async () => { const originalWindow = globalThis.window globalThis.window = {} as any expect(() => { createStore(devtools(() => ({ count: 0 }), { enabled: true })) }).not.toThrow() globalThis.window = originalWindow }) }) it('preserves isRecording after setting from devtools', async () => { const api = createStore(devtools(() => ({ count: 0 }), { enabled: true })) const [connection] = getNamedConnectionApis(undefined) const [connectionSubscriber] = getNamedConnectionSubscribers(undefined) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'PAUSE_RECORDING' }, }) connectionSubscriber({ type: 'ACTION', payload: '{ "type": "__setState", "state": { "foo": "bar" } }', }) api.setState({ count: 1 }) expect(connection.send).not.toBeCalled() }) /* features: * [] if name is undefined - use multiple devtools connections. * [] if name and store is defined - use connection for specific 'name'. * [] if two stores are connected to one 'name' group and. * another connected to another 'name' group, then feature should work * [] check actions with this feature, for multiple stores that store prefixes are added - * [] - reset * [] - commit * [] - rollback * [] - jump to state, jump to action * [] - import state **/ describe('when redux connection was called on multiple stores with `name` undefined in `devtools` options', () => { it('should create separate connection for each devtools store with .connect call', async () => { const options1 = { foo: 'bar', testConnectionId: 'asdf' } const options2 = { foo: 'barr', testConnectionId: '123asd' } const initialState1 = { count: 0 } const initialState2 = { count1: 1 } createStore(devtools(() => initialState1, { enabled: true, ...options1 })) createStore(devtools(() => initialState2, { enabled: true, ...options2 })) expect(extensionConnector.connect).toHaveBeenNthCalledWith(1, options1) expect(extensionConnector.connect).toHaveBeenNthCalledWith(2, options2) }) it('should call .init on each different connection object', async () => { const options1 = { foo: 'bar', testConnectionId: 'asdf' } const options2 = { foo: 'barr', testConnectionId: '123asd' } const initialState1 = { count: 0 } const initialState2 = { count1: 1 } createStore(devtools(() => initialState1, { enabled: true, ...options1 })) createStore(devtools(() => initialState2, { enabled: true, ...options2 })) const [conn1, conn2] = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, ) expect(conn1.init).toHaveBeenCalledWith(initialState1) expect(conn2.init).toHaveBeenCalledWith(initialState2) }) describe('when `store` property was provided in `devtools` call in options', () => { it('should create single connection for all internal calls of .connect and `store` is not passed to .connect', async () => { const { devtools: newDevtools } = await import('zustand/middleware') const options1 = { store: 'store1123', foo: 'bar1' } const options2 = { store: 'store2313132', foo: 'bar2' } const initialState1 = { count: 0 } const initialState2 = { count1: 1 } createStore( newDevtools(() => initialState1, { enabled: true, ...options1 }), ) createStore( newDevtools(() => initialState2, { enabled: true, ...options2 }), ) expect(extensionConnector.connect).toHaveBeenCalledTimes(1) expect(extensionConnector.connect).toHaveBeenCalledWith({ foo: options1.foo, }) }) it('should call `.init` on single connection with combined states after each `create(devtools` call', async () => { const { devtools: newDevtools } = await import('zustand/middleware') const options1 = { store: 'store12' } const options2 = { store: 'store21' } const initialState1 = { count1: 0 } const initialState2 = { count2: 1 } createStore( newDevtools(() => initialState1, { enabled: true, ...options1 }), ) createStore( newDevtools(() => initialState2, { enabled: true, ...options2 }), ) expect(extensionConnector.connect).toHaveBeenCalledTimes(1) const [connection] = getNamedConnectionApis(undefined) expect(connection.init).toHaveBeenCalledTimes(2) expect(connection.init).toHaveBeenNthCalledWith(1, { [options1.store]: initialState1, }) expect(connection.init).toHaveBeenNthCalledWith(2, { [options1.store]: initialState1, [options2.store]: initialState2, }) }) }) }) describe('when redux connection was called on multiple stores with `name` provided in `devtools` options', () => { describe('when same `name` is provided to all stores in devtools options', () => { it('should call .connect of redux extension with `name` that was passed from `devtools` options', async () => { const connectionName = 'test' const options1 = { name: connectionName, store: 'store1123', foo: 'bar1' } const options2 = { name: connectionName, store: 'store1414', foo: 'bar1' } const initialState1 = { count: 0 } const initialState2 = { count: 2 } createStore(devtools(() => initialState1, { enabled: true, ...options1 })) createStore(devtools(() => initialState2, { enabled: true, ...options2 })) expect(extensionConnector.connect).toHaveBeenCalledTimes(1) expect(extensionConnector.connect).toHaveBeenCalledWith({ foo: options1.foo, name: connectionName, }) }) }) describe('when different `name` props were provided for different group of stores in devtools options', () => { it('should call .connect of redux extension with `name` that was passed from `devtools` options', async () => { const connectionNameGroup1 = 'test1' const connectionNameGroup2 = 'test2' const options1 = { name: connectionNameGroup1, store: 'store1123', foo: 'bar2', } const options2 = { name: connectionNameGroup1, store: 'store1232', foo: 'bar3', } const options3 = { name: connectionNameGroup2, store: 'store61661', foo: 'bar4', } const options4 = { name: connectionNameGroup2, store: 'store14632', foo: 'bar5', } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const initialState3 = { count: 5 } const initialState4 = { count: 7 } createStore(devtools(() => initialState1, { enabled: true, ...options1 })) createStore(devtools(() => initialState2, { enabled: true, ...options2 })) createStore(devtools(() => initialState3, { enabled: true, ...options3 })) createStore(devtools(() => initialState4, { enabled: true, ...options4 })) expect(extensionConnector.connect).toHaveBeenCalledTimes(2) expect(extensionConnector.connect).toHaveBeenNthCalledWith(1, { foo: options1.foo, name: connectionNameGroup1, }) expect(extensionConnector.connect).toHaveBeenNthCalledWith(2, { foo: options3.foo, name: connectionNameGroup2, }) }) it('should call `.init` on single connection with combined states after each `create(devtools` call', async () => { const { devtools: newDevtools } = await import('zustand/middleware') const connectionNameGroup1 = 'test1' const connectionNameGroup2 = 'test2' const options1 = { name: connectionNameGroup1, store: 'store1123', foo: 'bar2', } const options2 = { name: connectionNameGroup1, store: 'store1232', foo: 'bar3', } const options3 = { name: connectionNameGroup2, store: 'store61661', foo: 'bar4', } const options4 = { name: connectionNameGroup2, store: 'store14632', foo: 'bar5', } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const initialState3 = { count: 5 } const initialState4 = { count: 7 } createStore( newDevtools(() => initialState1, { enabled: true, ...options1 }), ) createStore( newDevtools(() => initialState2, { enabled: true, ...options2 }), ) createStore( newDevtools(() => initialState3, { enabled: true, ...options3 }), ) createStore( newDevtools(() => initialState4, { enabled: true, ...options4 }), ) expect(extensionConnector.connect).toHaveBeenCalledTimes(2) const [connection1, connection2] = getNamedConnectionApis( connectionNameGroup1, connectionNameGroup2, ) expect(connection1.init).toHaveBeenCalledTimes(2) expect(connection1.init).toHaveBeenNthCalledWith(1, { [options1.store]: initialState1, }) expect(connection1.init).toHaveBeenNthCalledWith(2, { [options1.store]: initialState1, [options2.store]: initialState2, }) expect(connection2.init).toHaveBeenCalledTimes(2) expect(connection2.init).toHaveBeenNthCalledWith(1, { [options3.store]: initialState3, }) expect(connection2.init).toHaveBeenNthCalledWith(2, { [options3.store]: initialState3, [options4.store]: initialState4, }) }) it('preserves isRecording after setting from devtools on proper connection subscriber', async () => { const options1 = { name: 'asdf1' } const options2 = { name: 'asdf2' } const api1 = createStore( devtools(() => ({ count: 0 }), { enabled: true, ...options1 }), ) createStore( devtools(() => ({ count: 0 }), { enabled: true, ...options2 }), ) const connections = getNamedConnectionApis(options1.name, options2.name) const [connectionSubscriber] = getNamedConnectionSubscribers( options1.name, ) connectionSubscriber({ type: 'DISPATCH', payload: { type: 'PAUSE_RECORDING' }, }) connectionSubscriber({ type: 'ACTION', payload: '{ "type": "__setState", "state": { "foo": "bar" } }', }) api1.setState({ count: 1 }) connections.forEach((conn) => expect(conn.send).not.toBeCalled()) }) describe('with redux middleware', () => { let api1: StoreApi<{ count: number dispatch: ( action: { type: 'INCREMENT' } | { type: 'DECREMENT' }, ) => { type: 'INCREMENT' } | { type: 'DECREMENT' } }> let api2: StoreApi<{ count: number dispatch: ( action: { type: 'INCREMENT' } | { type: 'DECREMENT' }, ) => { type: 'INCREMENT' } | { type: 'DECREMENT' } }> it('works as expected', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } api1 = createStore( devtools( redux( ( { count }, { type }: { type: 'INCREMENT' } | { type: 'DECREMENT' }, ) => ({ count: count + (type === 'INCREMENT' ? 1 : -1), }), { count: 0 }, ), { enabled: true, ...options1 }, ), ) api2 = createStore( devtools( redux( ( { count }, { type }: { type: 'INCREMENT' } | { type: 'DECREMENT' }, ) => ({ count: count + (type === 'INCREMENT' ? 1 : -1), }), { count: 10 }, ), { enabled: true, ...options2 }, ), ) ;(api1 as any).dispatch({ type: 'INCREMENT' }) ;(api1 as any).dispatch({ type: 'INCREMENT' }) ;(api2 as any).dispatch({ type: 'INCREMENT' }) ;(api2 as any).dispatch({ type: 'INCREMENT' }) const [connection1, connection2] = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, ) const [connectionSubscriber1, connectionSubscriber2] = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, ) connectionSubscriber1({ type: 'ACTION', payload: JSON.stringify({ type: 'DECREMENT' }), }) connectionSubscriber2({ type: 'ACTION', payload: JSON.stringify({ type: 'DECREMENT' }), }) expect(connection1.init.mock.calls).toMatchObject([ [{ count: 0 }] as unknown as Record, ]) expect(connection2.init.mock.calls).toMatchObject([ [{ count: 10 }] as unknown as Record, ]) expect(connection1.send.mock.calls).toMatchObject([ [{ type: 'INCREMENT' }, { count: 1 }] as unknown as Record< string, unknown >, [{ type: 'INCREMENT' }, { count: 2 }] as unknown as Record< string, unknown >, [{ type: 'DECREMENT' }, { count: 1 }] as unknown as Record< string, unknown >, ]) expect(connection2.send.mock.calls).toMatchObject([ [{ type: 'INCREMENT' }, { count: 11 }] as unknown as Record< string, unknown >, [{ type: 'INCREMENT' }, { count: 12 }] as unknown as Record< string, unknown >, [{ type: 'DECREMENT' }, { count: 11 }] as unknown as Record< string, unknown > as unknown as Record, ]) expect(api1.getState()).toMatchObject({ count: 1 }) expect(api2.getState()).toMatchObject({ count: 11 }) }) }) }) }) describe('when create devtools was called multiple times with `name` option undefined', () => { describe('When state changes...', () => { it("sends { type: setStateName || 'anonymous`, ...rest } as the action with current state, isolated from other connections", async () => { const options1 = { enabled: true, testConnectionId: '123', } const options2 = { enabled: true, testConnectionId: '324', } const options3 = { enabled: true, testConnectionId: '412', } const api1 = createStore( devtools(() => ({ count: 0, foo: 'bar' }), options1), ) createStore(devtools(() => ({ count: 0, foo: 'bar1' }), options2)) createStore(devtools(() => ({ count: 0, foo: 'bar2' }), options3)) api1.setState({ count: 10 }, false, 'testSetStateName') const [connection1, connection2, connection3] = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) expect(connection1.send).toHaveBeenLastCalledWith( { type: 'testSetStateName' }, { count: 10, foo: 'bar' }, ) expect(connection2.send).not.toBeCalled() expect(connection3.send).not.toBeCalled() api1.setState({ count: 15 }, false, { type: 'testSetStateName', payload: 15, }) expect(connection1.send).toHaveBeenLastCalledWith( { type: 'testSetStateName', payload: 15 }, { count: 15, foo: 'bar' }, ) expect(connection2.send).not.toBeCalled() expect(connection3.send).not.toBeCalled() api1.setState({ count: 5, foo: 'baz' }, true) expect(connection1.send).toHaveBeenLastCalledWith( { type: 'anonymous' }, { count: 5, foo: 'baz' }, ) expect(connection2.send).not.toBeCalled() expect(connection3.send).not.toBeCalled() }) }) describe('when it receives a message of type...', () => { describe('ACTION...', () => { it('does nothing, connections isolated from each other', async () => { const options1 = { testConnectionId: '123' } const options2 = { testConnectionId: '231' } const options3 = { testConnectionId: '4342' } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const initialState3 = { count: 3 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1, }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2, }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3, }), ) const setState1 = vi.spyOn(api1, 'setState') const setState2 = vi.spyOn(api2, 'setState') const setState3 = vi.spyOn(api3, 'setState') const [subscriber] = getUnnamedConnectionSubscribers( options1.testConnectionId, ) subscriber({ type: 'ACTION', payload: '{ "type": "INCREMENT" }', }) expect(api1.getState()).toBe(initialState1) expect(api2.getState()).toBe(initialState2) expect(api3.getState()).toBe(initialState3) expect(setState1).not.toBeCalled() expect(setState2).not.toBeCalled() expect(setState3).not.toBeCalled() }) it('unless action type is __setState, connections isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const initialState3 = { count: 5 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) const [connectionSubscriber] = getUnnamedConnectionSubscribers( options1.testConnectionId, ) connectionSubscriber({ type: 'ACTION', payload: '{ "type": "__setState", "state": { "foo": "bar" } }', }) expect(api1.getState()).toStrictEqual({ ...initialState1, foo: 'bar' }) expect(api2.getState()).toStrictEqual({ ...initialState2 }) expect(api3.getState()).toStrictEqual({ ...initialState3 }) }) it('does nothing even if there is `api.dispatch`, connections isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const initialState3 = { count: 5 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) ;(api1 as any).dispatch = vi.fn() ;(api2 as any).dispatch = vi.fn() ;(api3 as any).dispatch = vi.fn() const setState1 = vi.spyOn(api1, 'setState') const setState2 = vi.spyOn(api2, 'setState') const setState3 = vi.spyOn(api3, 'setState') const subscribers = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) const testPayload = { type: 'ACTION', payload: '{ "type": "INCREMENT" }', } subscribers.forEach((sub) => sub(testPayload)) expect(api1.getState()).toBe(initialState1) expect(api2.getState()).toBe(initialState2) expect(api3.getState()).toBe(initialState3) expect(setState1).not.toBeCalled() expect(setState2).not.toBeCalled() expect(setState3).not.toBeCalled() expect((api1 as any).dispatch).not.toBeCalled() expect((api2 as any).dispatch).not.toBeCalled() expect((api3 as any).dispatch).not.toBeCalled() }) it('dispatches with `api.dispatch` when `api.dispatchFromDevtools` is set to true, connections are isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const initialState3 = { count: 5 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) ;(api1 as any).dispatch = vi.fn() ;(api1 as any).dispatchFromDevtools = true ;(api2 as any).dispatch = vi.fn() ;(api2 as any).dispatchFromDevtools = true ;(api3 as any).dispatch = vi.fn() ;(api3 as any).dispatchFromDevtools = true const setState1 = vi.spyOn(api1, 'setState') const setState2 = vi.spyOn(api2, 'setState') const setState3 = vi.spyOn(api3, 'setState') const subscribers = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) const getTestPayload = (n: number) => ({ type: 'ACTION', payload: `{ "type": "INCREMENT${n}" }`, }) subscribers.forEach((sub, i) => sub(getTestPayload(i + 1))) expect(api1.getState()).toBe(initialState1) expect(api2.getState()).toBe(initialState2) expect(api3.getState()).toBe(initialState3) expect(setState1).not.toBeCalled() expect(setState2).not.toBeCalled() expect(setState3).not.toBeCalled() expect((api1 as any).dispatch).toHaveBeenLastCalledWith({ type: 'INCREMENT1', }) expect((api2 as any).dispatch).toHaveBeenLastCalledWith({ type: 'INCREMENT2', }) expect((api3 as any).dispatch).toHaveBeenLastCalledWith({ type: 'INCREMENT3', }) }) it('does not throw for unsupported payload, connections are isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const initialState3 = { count: 5 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) ;(api1 as any).dispatch = vi.fn() ;(api1 as any).dispatchFromDevtools = true ;(api2 as any).dispatch = vi.fn() ;(api2 as any).dispatchFromDevtools = true ;(api3 as any).dispatch = vi.fn() ;(api3 as any).dispatchFromDevtools = true const setState1 = vi.spyOn(api1, 'setState') const setState2 = vi.spyOn(api2, 'setState') const setState3 = vi.spyOn(api3, 'setState') const originalConsoleError = console.error console.error = vi.fn() const [ connectionSubscriber1, connectionSubscriber2, connectionSubscriber3, ] = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) expect(() => { connectionSubscriber1({ type: 'ACTION', payload: 'this.increment1()', }) }).not.toThrow() expect(console.error).toHaveBeenNthCalledWith( 1, '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('this.increment1()') } catch (e) { return e } })(), ) expect(() => { connectionSubscriber1({ type: 'ACTION', payload: 'this.increment2()', }) }).not.toThrow() expect(console.error).toHaveBeenNthCalledWith( 2, '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('this.increment2()') } catch (e) { return e } })(), ) expect(() => { connectionSubscriber1({ type: 'ACTION', payload: 'this.increment3()', }) }).not.toThrow() expect(console.error).toHaveBeenNthCalledWith( 3, '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('this.increment3()') } catch (e) { return e } })(), ) expect(() => { connectionSubscriber1({ type: 'ACTION', payload: { name: 'increment', args: [] }, }) }).not.toThrow() expect(console.error).toHaveBeenNthCalledWith( 4, '[zustand devtools middleware] Unsupported action format', ) expect(() => { connectionSubscriber2({ type: 'ACTION', payload: { name: 'increment', args: [] }, }) }).not.toThrow() expect(console.error).toHaveBeenNthCalledWith( 5, '[zustand devtools middleware] Unsupported action format', ) expect(() => { connectionSubscriber3({ type: 'ACTION', payload: { name: 'increment', args: [] }, }) }).not.toThrow() expect(console.error).toHaveBeenNthCalledWith( 6, '[zustand devtools middleware] Unsupported action format', ) expect(api1.getState()).toBe(initialState1) expect(api2.getState()).toBe(initialState2) expect(api3.getState()).toBe(initialState3) expect(setState1).not.toBeCalled() expect(setState2).not.toBeCalled() expect(setState3).not.toBeCalled() expect((api1 as any).dispatch).not.toBeCalled() expect((api2 as any).dispatch).not.toBeCalled() expect((api3 as any).dispatch).not.toBeCalled() console.error = originalConsoleError }) }) describe('DISPATCH and payload of type...', () => { it('RESET, it inits with initial state, connections are isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const initialState3 = { count: 5 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) api1.setState({ count: 1 }) api2.setState({ count: 3 }) api3.setState({ count: 10 }) const connections = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) const [connection1, connection2, connection3] = connections connections.forEach((conn) => conn.send.mockClear()) const subscribers = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) const action = { type: 'DISPATCH', payload: { type: 'RESET' }, } subscribers.forEach((sub) => sub(action)) expect(api1.getState()).toStrictEqual(initialState1) expect(api1.getState()).toStrictEqual(initialState1) expect(api1.getState()).toStrictEqual(initialState1) expect(connection1.init).toHaveBeenLastCalledWith(initialState1) expect(connection2.init).toHaveBeenLastCalledWith(initialState2) expect(connection3.init).toHaveBeenLastCalledWith(initialState3) connections.forEach((conn) => expect(conn.send).not.toBeCalled()) }) it('COMMIT, it inits with current state, connections are isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const initialState3 = { count: 5 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) api1.setState({ count: 1 }) api2.setState({ count: 3 }) api3.setState({ count: 10 }) const currentState1 = api1.getState() const currentState2 = api2.getState() const currentState3 = api3.getState() const connections = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connections.forEach((conn) => conn.send.mockClear()) const subscribers = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) const action = { type: 'DISPATCH', payload: { type: 'COMMIT' }, } subscribers.forEach((sub) => sub(action)) const [connection1, connection2, connection3] = connections expect(connection1.init).toHaveBeenLastCalledWith(currentState1) expect(connection2.init).toHaveBeenLastCalledWith(currentState2) expect(connection3.init).toHaveBeenLastCalledWith(currentState3) connections.forEach((conn) => expect(conn.send).not.toBeCalled()) }) }) describe('ROLLBACK...', () => { it('updates state without recording and inits with `message.state, connections are isolated from each other`', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0, increment: () => {} } const initialState2 = { count: 2, increment: () => {} } const initialState3 = { count: 5, increment: () => {} } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) const newState1 = { foo: 'bar1' } const newState2 = { foo: 'bar2' } const newState3 = { foo: 'bar3' } const connections = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connections.forEach((conn) => conn.send.mockClear()) const [ connectionSubscriber1, connectionSubscriber2, connectionSubscriber3, ] = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connectionSubscriber1({ type: 'DISPATCH', payload: { type: 'ROLLBACK' }, state: JSON.stringify(newState1), }) connectionSubscriber2({ type: 'DISPATCH', payload: { type: 'ROLLBACK' }, state: JSON.stringify(newState2), }) connectionSubscriber3({ type: 'DISPATCH', payload: { type: 'ROLLBACK' }, state: JSON.stringify(newState3), }) expect(api1.getState()).toStrictEqual({ ...initialState1, ...newState1, }) expect(api2.getState()).toStrictEqual({ ...initialState2, ...newState2, }) expect(api3.getState()).toStrictEqual({ ...initialState3, ...newState3, }) const [connection1, connection2, connection3] = connections expect(connection1.init).toHaveBeenLastCalledWith({ ...initialState1, ...newState1, }) expect(connection2.init).toHaveBeenLastCalledWith({ ...initialState2, ...newState2, }) expect(connection3.init).toHaveBeenLastCalledWith({ ...initialState3, ...newState3, }) connections.forEach((conn) => expect(conn.send).not.toBeCalled()) }) it('does not throw for unparsable `message.state`, connections are isolated from each other', async () => { const increment1 = () => {} const increment2 = () => {} const increment3 = () => {} const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0, increment: increment1 } const initialState2 = { count: 2, increment: increment2 } const initialState3 = { count: 5, increment: increment3 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) const originalConsoleError = console.error console.error = vi.fn() const connections = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connections.forEach((conn) => conn.init.mockClear()) connections.forEach((conn) => conn.send.mockClear()) const [ connectionSubscriber1, connectionSubscriber2, connectionSubscriber3, ] = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connectionSubscriber1({ type: 'DISPATCH', payload: { type: 'ROLLBACK' }, state: 'foobar', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar') } catch (e) { return e } })(), ) connectionSubscriber2({ type: 'DISPATCH', payload: { type: 'ROLLBACK' }, state: 'foobar1', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar1') } catch (e) { return e } })(), ) connectionSubscriber3({ type: 'DISPATCH', payload: { type: 'ROLLBACK' }, state: 'foobar3', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar3') } catch (e) { return e } })(), ) expect(api1.getState()).toBe(initialState1) expect(api2.getState()).toBe(initialState2) expect(api3.getState()).toBe(initialState3) connections.forEach((conn) => { expect(conn.init).not.toBeCalled() expect(conn.send).not.toBeCalled() }) console.error = originalConsoleError }) }) describe('JUMP_TO_STATE...', () => { const increment1 = () => {} const increment2 = () => {} const increment3 = () => {} it('updates state without recording with `message.state`, connections are isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0, increment: increment1 } const initialState2 = { count: 2, increment: increment2 } const initialState3 = { count: 5, increment: increment3 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) const newState1 = { foo: 'bar1' } const newState2 = { foo: 'bar2' } const newState3 = { foo: 'bar3' } const connections = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connections.forEach((conn) => conn.send.mockClear()) const [ connectionSubscriber1, connectionSubscriber2, connectionSubscriber3, ] = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connectionSubscriber1({ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE' }, state: JSON.stringify(newState1), }) connectionSubscriber2({ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE' }, state: JSON.stringify(newState2), }) connectionSubscriber3({ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE' }, state: JSON.stringify(newState3), }) expect(api1.getState()).toStrictEqual({ ...initialState1, ...newState1, }) expect(api2.getState()).toStrictEqual({ ...initialState2, ...newState2, }) expect(api3.getState()).toStrictEqual({ ...initialState3, ...newState3, }) connections.forEach((conn) => expect(conn.send).not.toBeCalled()) }) it('does not throw for unparsable `message.state`, connections are isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0, increment: increment1 } const initialState2 = { count: 2, increment: increment2 } const initialState3 = { count: 5, increment: increment3 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) const originalConsoleError = console.error console.error = vi.fn() const connections = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connections.forEach((conn) => conn.send.mockClear()) const [ connectionSubscriber1, connectionSubscriber2, connectionSubscriber3, ] = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connectionSubscriber1({ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE' }, state: 'foobar', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar') } catch (e) { return e } })(), ) connectionSubscriber2({ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE' }, state: 'foobar2', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar2') } catch (e) { return e } })(), ) connectionSubscriber3({ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE' }, state: 'foobar3', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar3') } catch (e) { return e } })(), ) expect(api1.getState()).toBe(initialState1) expect(api2.getState()).toBe(initialState2) expect(api3.getState()).toBe(initialState3) connections.forEach((conn) => expect(conn.send).not.toBeCalled()) console.error = originalConsoleError }) }) describe('JUMP_TO_ACTION...', () => { const increment1 = () => {} const increment2 = () => {} const increment3 = () => {} it('updates state without recording with `message.state`, connections are isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0, increment: increment1 } const initialState2 = { count: 2, increment: increment2 } const initialState3 = { count: 5, increment: increment3 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) const newState1 = { foo: 'bar1' } const newState2 = { foo: 'bar2' } const newState3 = { foo: 'bar3' } const connections = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connections.forEach((conn) => conn.send.mockClear()) const [ connectionSubscriber1, connectionSubscriber2, connectionSubscriber3, ] = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connectionSubscriber1({ type: 'DISPATCH', payload: { type: 'JUMP_TO_ACTION' }, state: JSON.stringify(newState1), }) connectionSubscriber2({ type: 'DISPATCH', payload: { type: 'JUMP_TO_ACTION' }, state: JSON.stringify(newState2), }) connectionSubscriber3({ type: 'DISPATCH', payload: { type: 'JUMP_TO_ACTION' }, state: JSON.stringify(newState3), }) expect(api1.getState()).toStrictEqual({ ...initialState1, ...newState1, }) expect(api2.getState()).toStrictEqual({ ...initialState2, ...newState2, }) expect(api3.getState()).toStrictEqual({ ...initialState3, ...newState3, }) connections.forEach((conn) => expect(conn.send).not.toBeCalled()) }) it('does not throw for unparsable `message.state`, connections are isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0, increment: increment1 } const initialState2 = { count: 2, increment: increment2 } const initialState3 = { count: 5, increment: increment3 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) const originalConsoleError = console.error console.error = vi.fn() const connections = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connections.forEach((conn) => conn.send.mockClear()) const [ connectionSubscriber1, connectionSubscriber2, connectionSubscriber3, ] = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connectionSubscriber1({ type: 'DISPATCH', payload: { type: 'JUMP_TO_ACTION' }, state: 'foobar', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar') } catch (e) { return e } })(), ) connectionSubscriber2({ type: 'DISPATCH', payload: { type: 'JUMP_TO_ACTION' }, state: 'foobar2', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar2') } catch (e) { return e } })(), ) connectionSubscriber3({ type: 'DISPATCH', payload: { type: 'JUMP_TO_ACTION' }, state: 'foobar3', }) expect(console.error).toHaveBeenLastCalledWith( '[zustand devtools middleware] Could not parse the received json', (() => { try { JSON.parse('foobar3') } catch (e) { return e } })(), ) expect(api1.getState()).toBe(initialState1) expect(api2.getState()).toBe(initialState2) expect(api3.getState()).toBe(initialState3) connections.forEach((conn) => expect(conn.send).not.toBeCalled()) console.error = originalConsoleError }) it('IMPORT_STATE, it updates state without recording and inits the last computedState, connections are isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const initialState1 = { count: 0, increment: increment1 } const initialState2 = { count: 2, increment: increment2 } const initialState3 = { count: 5, increment: increment3 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options3 }), ) const nextLiftedState1 = { computedStates: [{ state: { count: 4 } }, { state: { count: 5 } }], } const nextLiftedState2 = { computedStates: [{ state: { count: 20 } }, { state: { count: 8 } }], } const nextLiftedState3 = { computedStates: [{ state: { count: 12 } }, { state: { count: 100 } }], } const connections = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connections.forEach((conn) => conn.send.mockClear()) const [ connectionSubscriber1, connectionSubscriber2, connectionSubscriber3, ] = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) connectionSubscriber1({ type: 'DISPATCH', payload: { type: 'IMPORT_STATE', nextLiftedState: nextLiftedState1, }, }) connectionSubscriber2({ type: 'DISPATCH', payload: { type: 'IMPORT_STATE', nextLiftedState: nextLiftedState2, }, }) connectionSubscriber3({ type: 'DISPATCH', payload: { type: 'IMPORT_STATE', nextLiftedState: nextLiftedState3, }, }) expect(api1.getState()).toStrictEqual({ ...initialState1, ...nextLiftedState1.computedStates.slice(-1)[0]?.state, }) expect(api2.getState()).toStrictEqual({ ...initialState2, ...nextLiftedState2.computedStates.slice(-1)[0]?.state, }) expect(api3.getState()).toStrictEqual({ ...initialState3, ...nextLiftedState3.computedStates.slice(-1)[0]?.state, }) const [connection1, connection2, connection3] = connections expect(connection1.send).toHaveBeenLastCalledWith( null, nextLiftedState1, ) expect(connection2.send).toHaveBeenLastCalledWith( null, nextLiftedState2, ) expect(connection3.send).toHaveBeenLastCalledWith( null, nextLiftedState3, ) }) it('PAUSE_RECORDING, it toggles the sending of actions, connections are isolated from each other', async () => { const options1 = { testConnectionId: 'asdf' } const options2 = { testConnectionId: '2f' } const options3 = { testConnectionId: 'd2e' } const api1 = createStore( devtools(() => ({ count: 0 }), { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => ({ count: 2 }), { enabled: true, ...options2 }), ) const api3 = createStore( devtools(() => ({ count: 4 }), { enabled: true, ...options3 }), ) const newState1 = { count: 1 } const newState2 = { count: 12 } const newState3 = { count: 30 } api1.setState(newState1, false, 'increment') api2.setState(newState2, false, 'increment') api3.setState(newState3, false, 'increment') const [connection1, connection2, connection3] = getUnnamedConnectionApis( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) const [ connectionSubscriber1, connectionSubscriber2, connectionSubscriber3, ] = getUnnamedConnectionSubscribers( options1.testConnectionId, options2.testConnectionId, options3.testConnectionId, ) expect(connection1.send).toHaveBeenLastCalledWith( { type: 'increment' }, newState1, ) connectionSubscriber1({ type: 'DISPATCH', payload: { type: 'PAUSE_RECORDING' }, }) api1.setState({ count: 2 }, false, 'increment') expect(connection1.send).toHaveBeenLastCalledWith( { type: 'increment' }, newState1, ) connectionSubscriber1({ type: 'DISPATCH', payload: { type: 'PAUSE_RECORDING' }, }) api1.setState({ count: 3 }, false, 'increment') expect(connection1.send).toHaveBeenLastCalledWith( { type: 'increment' }, { count: 3 }, ) expect(connection2.send).toHaveBeenLastCalledWith( { type: 'increment' }, newState2, ) connectionSubscriber2({ type: 'DISPATCH', payload: { type: 'PAUSE_RECORDING' }, }) api2.setState({ count: 2 }, false, 'increment') expect(connection2.send).toHaveBeenLastCalledWith( { type: 'increment' }, newState2, ) connectionSubscriber2({ type: 'DISPATCH', payload: { type: 'PAUSE_RECORDING' }, }) api2.setState({ count: 3 }, false, 'increment') expect(connection2.send).toHaveBeenLastCalledWith( { type: 'increment' }, { count: 3 }, ) expect(connection3.send).toHaveBeenLastCalledWith( { type: 'increment' }, newState3, ) connectionSubscriber3({ type: 'DISPATCH', payload: { type: 'PAUSE_RECORDING' }, }) api3.setState({ count: 2 }, false, 'increment') expect(connection3.send).toHaveBeenLastCalledWith( { type: 'increment' }, newState3, ) connectionSubscriber3({ type: 'DISPATCH', payload: { type: 'PAUSE_RECORDING' }, }) api3.setState({ count: 3 }, false, 'increment') expect(connection3.send).toHaveBeenLastCalledWith( { type: 'increment' }, { count: 3 }, ) }) }) }) }) describe('when create devtools was called multiple times with `name` and `store` options defined', () => { describe('when `type` was provided in store state methods as option', () => { describe('When state changes...', () => { it("sends { type: setStateName || 'anonymous`, ...rest } as the action with current state", async () => { const options = { name: 'testOptionsName', store: 'someStore', enabled: true, } const api = createStore( devtools(() => ({ count: 0, foo: 'bar' }), options), ) const testStateActionType = 'testSetStateName' api.setState({ count: 10 }, false, testStateActionType) const [connection] = getNamedConnectionApis(options.name) expect(connection.send).toHaveBeenLastCalledWith( { type: `${options.store}/${testStateActionType}` }, { [options.store]: { count: 10, foo: 'bar' } }, ) api.setState({ count: 15 }, false, { type: testStateActionType, payload: 15, }) expect(connection.send).toHaveBeenLastCalledWith( { type: `${options.store}/${testStateActionType}`, payload: 15 }, { [options.store]: { count: 15, foo: 'bar' } }, ) api.setState({ count: 5, foo: 'baz' }, true) expect(connection.send).toHaveBeenLastCalledWith( { type: `${options.store}/anonymous` }, { [options.store]: { count: 5, foo: 'baz' } }, ) }) }) describe('when it receives a message of type...', () => { describe('ACTION...', () => { it('does nothing, connections isolated from each other', async () => { const options1 = { testConnectionId: '123', store: 'store1' } const options2 = { testConnectionId: '231', store: 'store2' } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const initialState3 = { count: 5 } const initialState4 = { count: 6 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1, }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options1, }), ) const api3 = createStore( devtools(() => initialState3, { enabled: true, ...options2, }), ) const api4 = createStore( devtools(() => initialState4, { enabled: true, ...options2, }), ) const setState1 = vi.spyOn(api1, 'setState') const setState2 = vi.spyOn(api2, 'setState') const setState3 = vi.spyOn(api3, 'setState') const setState4 = vi.spyOn(api4, 'setState') const [subscriber] = getUnnamedConnectionSubscribers( options1.testConnectionId, ) subscriber({ type: 'ACTION', payload: '{ "type": "INCREMENT" }', }) expect(api1.getState()).toBe(initialState1) expect(api2.getState()).toBe(initialState2) expect(api3.getState()).toBe(initialState3) expect(api4.getState()).toBe(initialState4) expect(setState1).not.toBeCalled() expect(setState2).not.toBeCalled() expect(setState3).not.toBeCalled() expect(setState4).not.toBeCalled() }) it('unless action type is __setState, connections isolated from each other', async () => { const name1 = 'name1' const name2 = 'name2' const store1 = 'someStore1' const store2 = 'someStore2' const options1 = { name: name1, store: store1, testStore: store1, } const options2 = { name: name2, store: store2, testStore: store2, } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const api1 = createStore( devtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( devtools(() => initialState2, { enabled: true, ...options2 }), ) const originalConsoleError = console.error console.error = vi.fn() const [connectionSubscriber] = getNamedConnectionSubscribers( getKeyFromOptions(options1), ) connectionSubscriber({ type: 'ACTION', payload: '{ "type": "__setState", "state": { "foo": "bar", "foo2": "bar2" } }', }) expect(console.error).toHaveBeenCalledWith( expect.stringContaining( '[zustand devtools middleware] Unsupported __setState', ), ) connectionSubscriber({ type: 'ACTION', payload: `{ "type": "__setState", "state": { "${options1.store}": { "foo": "bar" } } }`, }) expect(console.error).toHaveBeenCalledTimes(1) expect(api1.getState()).toStrictEqual({ ...initialState1, foo: 'bar', }) expect(api2.getState()).toStrictEqual({ ...initialState2 }) console.error = originalConsoleError }) it('does nothing even if there is `api.dispatch`, connections isolated from each other', async () => { const { devtools: newDevtools } = await import('zustand/middleware') const name1 = 'name1' const name2 = 'name2' const store1 = 'someStore1' const store2 = 'someStore2' const options1 = { name: name1, store: store1, testStore: store1, } const options2 = { name: name2, store: store2, testStore: store2, } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const api1 = createStore( newDevtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( newDevtools(() => initialState2, { enabled: true, ...options2 }), ) ;(api1 as any).dispatch = vi.fn() ;(api2 as any).dispatch = vi.fn() const setState1 = vi.spyOn(api1, 'setState') const setState2 = vi.spyOn(api2, 'setState') const subscribers = getNamedConnectionSubscribers( getKeyFromOptions(options1), getKeyFromOptions(options2), ) const testPayload = { type: 'ACTION', payload: '{ "type": "INCREMENT" }', } subscribers.forEach((sub) => sub(testPayload)) expect(api1.getState()).toBe(initialState1) expect(api2.getState()).toBe(initialState2) expect(setState1).not.toBeCalled() expect(setState2).not.toBeCalled() expect((api1 as any).dispatch).not.toBeCalled() expect((api2 as any).dispatch).not.toBeCalled() }) it('dispatches with `api.dispatch` when `api.dispatchFromDevtools` is set to true, connections are isolated from each other', async () => { const { devtools: newDevtools } = await import('zustand/middleware') const name1 = 'name1' const name2 = 'name2' const store1 = 'someStore1' const store2 = 'someStore2' const options1 = { name: name1, store: store1, testStore: store1, } const options2 = { name: name2, store: store2, testStore: store2, } const initialState1 = { count: 0 } const initialState2 = { count: 2 } const api1 = createStore( newDevtools(() => initialState1, { enabled: true, ...options1 }), ) const api2 = createStore( newDevtools(() => initialState2, { enabled: true, ...options2 }), ) ;(api1 as any).dispatch = vi.fn() ;(api1 as any).dispatchFromDevtools = true ;(api2 as any).dispatch = vi.fn() ;(api2 as any).dispatchFromDevtools = true const setState1 = vi.spyOn(api1, 'setState') const setState2 = vi.spyOn(api2, 'setState') const subscribers = getNamedConnectionSubscribers( getKeyFromOptions(options1), getKeyFromOptions(options2), ) const getTestPayload = (n: number) => ({ type: 'ACTION', payload: `{ "type": "INCREMENT${n}" }`, }) subscribers.forEach((sub, i) => sub(getTestPayload(i + 1))) expect(api1.getState()).toBe(initialState1) expect(api2.getState()).toBe(initialState2) expect(setState1).not.toBeCalled() expect(setState2).not.toBeCalled() expect((api1 as any).dispatch).toHaveBeenLastCalledWith({ type: 'INCREMENT1', }) expect((api2 as any).dispatch).toHaveBeenLastCalledWith({ type: 'INCREMENT2', }) }) }) }) }) }) describe('cleanup', () => { it('should unsubscribe from devtools when cleanup is called', async () => { const options = { name: 'test' } const store = createStore(devtools(() => ({ count: 0 }), options)) const [connection] = getNamedConnectionApis(options.name) store.devtools.cleanup() expect(connection.unsubscribe).toHaveBeenCalledTimes(1) }) it('should remove store from tracked connection after cleanup', async () => { const options = { name: 'test-store-name', store: 'test-store-id', enabled: true, } const store1 = createStore(devtools(() => ({ count: 0 }), options)) store1.devtools.cleanup() const store2 = createStore(devtools(() => ({ count: 0 }), options)) const [connection] = getNamedConnectionApis(options.name) store2.setState({ count: 15 }, false, 'updateCount') expect(connection.send).toHaveBeenLastCalledWith( { type: `${options.store}/updateCount` }, { [options.store]: { count: 15 } }, ) store1.setState({ count: 20 }, false, 'ignoredAction') expect(connection.send).not.toHaveBeenLastCalledWith( { type: `${options.store}/ignoredAction` }, expect.anything(), ) }) }) describe('actionsDenylist', () => { it('should pass actionsDenylist option to Redux DevTools', async () => { const options = { name: 'test-filter', enabled: true, actionsDenylist: ['secret.*'], } createStore(devtools(() => ({ count: 0 }), options)) // Verify that actionsDenylist was passed to the connect call const extensionConnector = (window as any).__REDUX_DEVTOOLS_EXTENSION__ expect(extensionConnector.connect).toHaveBeenCalledWith( expect.objectContaining({ actionsDenylist: ['secret.*'], }), ) }) }) ================================================ FILE: tests/middlewareTypes.test.tsx ================================================ import { describe, expect, expectTypeOf, it } from 'vitest' import { create } from 'zustand' import type { StateCreator, StoreApi, StoreMutatorIdentifier } from 'zustand' import { combine, devtools, persist, redux, subscribeWithSelector, } from 'zustand/middleware' import { immer } from 'zustand/middleware/immer' import { createStore } from 'zustand/vanilla' type CounterState = { count: number inc: () => void } type ExampleStateCreator = < Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], U = T, >( f: StateCreator, ) => StateCreator type Write = Omit & U type StoreModifyAllButSetState = S extends { getState: () => infer T } ? Omit, 'setState'> : never declare module 'zustand/vanilla' { interface StoreMutators { 'org/example': Write> } } describe('counter state spec (no middleware)', () => { it('no middleware', () => { const useBoundStore = create((set, get) => ({ count: 0, inc: () => set({ count: get().count + 1 }, false), })) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) }) describe('counter state spec (single middleware)', () => { it('immer', () => { const useBoundStore = create()( immer((set, get) => ({ count: 0, inc: () => set((state) => { state.count = get().count + 1 }), })), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() const testSubtyping: StoreApi = createStore( immer(() => ({ count: 0 })), ) expect(testSubtyping).toBeDefined() const exampleMiddleware = ((initializer) => initializer) as ExampleStateCreator const testDerivedSetStateType = create()( exampleMiddleware( immer((set, get) => ({ count: 0, inc: () => set((state) => { state.count = get().count + 1 type OmitFn = Exclude any> expectTypeOf[0]>>().not.toExtend<{ additional: number }>() expectTypeOf>().toExtend<{ additional: number }>() }), })), ), ) expect(testDerivedSetStateType).toBeDefined() // the type of the `getState` should include our new property expectTypeOf(testDerivedSetStateType.getState()).toExtend<{ additional: number }>() // the type of the `setState` should not include our new property expectTypeOf< Parameters[0] >().not.toExtend<{ additional: number }>() }) it('redux', () => { const useBoundStore = create( redux<{ count: number }, { type: 'INC' }>( (state, action) => { switch (action.type) { case 'INC': return { ...state, count: state.count + 1 } default: return state } }, { count: 0 }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf( useBoundStore((s) => s.dispatch)({ type: 'INC' }), ).toEqualTypeOf<{ type: 'INC' }>() expectTypeOf(useBoundStore().dispatch({ type: 'INC' })).toEqualTypeOf<{ type: 'INC' }>() expectTypeOf(useBoundStore.dispatch({ type: 'INC' })).toEqualTypeOf<{ type: 'INC' }>() return <> } expect(TestComponent).toBeDefined() const testSubtyping: StoreApi = createStore( redux((x) => x, { count: 0 }), ) expect(testSubtyping).toBeDefined() }) it('devtools', () => { const useBoundStore = create()( devtools( (set, get) => ({ count: 0, inc: () => set({ count: get().count + 1 }, false, 'inc'), }), { name: 'prefix' }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() expectTypeOf( useBoundStore.setState({ count: 0 }, false, 'reset'), ).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() const testSubtyping: StoreApi = createStore( devtools(() => ({ count: 0 })), ) expect(testSubtyping).toBeDefined() }) it('devtools #2700', () => { type TableStore = { table: string } const useStoreA = create()( devtools((_set) => null, { name: 'table-storage' }), ) expect(useStoreA).toBeDefined() const useStoreB = create()( devtools(() => null, { name: 'table-storage' }), ) expect(useStoreB).toBeDefined() const useStoreC = create()((_set) => null) expect(useStoreC).toBeDefined() const useStoreD = create()(() => null) expect(useStoreD).toBeDefined() }) it('subscribeWithSelector', () => { const useBoundStore = create()( subscribeWithSelector((set, get) => ({ count: 1, inc: () => set({ count: get().count + 1 }, false), })), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() useBoundStore.subscribe( (state) => state.count, (count) => console.log(count * 2), ) return <> } expect(TestComponent).toBeDefined() const testSubtyping: StoreApi = createStore( subscribeWithSelector(() => ({ count: 0 })), ) expect(testSubtyping).toBeDefined() }) it('combine', () => { const useBoundStore = create( combine({ count: 1 }, (set, get) => ({ inc: () => set({ count: get().count + 1 }, false), })), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() const testSubtyping: StoreApi = createStore( combine({ count: 0 }, () => ({})), ) expect(testSubtyping).toBeDefined() }) it('persist', () => { const useBoundStore = create()( persist( (set, get) => ({ count: 1, inc: () => set({ count: get().count + 1 }, false), }), { name: 'prefix' }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() useBoundStore.persist.hasHydrated() return <> } expect(TestComponent).toBeDefined() const testSubtyping: StoreApi = createStore( persist(() => ({ count: 0 }), { name: 'prefix' }), ) expect(testSubtyping).toBeDefined() }) it('persist with partialize', () => { const useBoundStore = create()( persist( (set, get) => ({ count: 1, inc: () => set({ count: get().count + 1 }, false), }), { name: 'prefix', partialize: (s) => s.count }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() useBoundStore.persist.hasHydrated() useBoundStore.persist.setOptions({ // @ts-expect-error to test if the partialized state is inferred as number partialize: () => 'not-a-number', }) return <> } expect(TestComponent).toBeDefined() }) it('persist without custom api (#638)', () => { const useBoundStore = create()( persist( (set, get) => ({ count: 1, inc: () => set({ count: get().count + 1 }, false), }), { name: 'prefix' }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) }) describe('counter state spec (double middleware)', () => { it('immer & devtools', () => { const useBoundStore = create()( immer( devtools( (set, get) => ({ count: 0, inc: () => set( (state) => { state.count = get().count + 1 }, false, { type: 'inc', by: 1 }, ), }), { name: 'prefix' }, ), ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() expectTypeOf( useBoundStore.setState({ count: 0 }, false, 'reset'), ).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) it('devtools & redux', () => { const useBoundStore = create( devtools( redux( (state, action: { type: 'INC' }) => { switch (action.type) { case 'INC': return { ...state, count: state.count + 1 } default: return state } }, { count: 0 }, ), { name: 'prefix' }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf( useBoundStore((s) => s.dispatch)({ type: 'INC' }), ).toEqualTypeOf<{ type: 'INC' }>() expectTypeOf(useBoundStore().dispatch({ type: 'INC' })).toEqualTypeOf<{ type: 'INC' }>() expectTypeOf(useBoundStore.dispatch({ type: 'INC' })).toEqualTypeOf<{ type: 'INC' }>() expectTypeOf( useBoundStore.setState({ count: 0 }, false, 'reset'), ).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) it('devtools & combine', () => { const useBoundStore = create( devtools( combine({ count: 1 }, (set, get) => ({ inc: () => set({ count: get().count + 1 }, false, 'inc'), })), { name: 'prefix' }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() expectTypeOf( useBoundStore.setState({ count: 0 }, false, 'reset'), ).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) it('subscribeWithSelector & combine', () => { const useBoundStore = create( subscribeWithSelector( combine({ count: 1 }, (set, get) => ({ inc: () => set({ count: get().count + 1 }, false), })), ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() useBoundStore.subscribe( (state) => state.count, (count) => console.log(count * 2), ) return <> } expect(TestComponent).toBeDefined() }) it('devtools & subscribeWithSelector', () => { const useBoundStore = create()( devtools( subscribeWithSelector((set, get) => ({ count: 1, inc: () => set({ count: get().count + 1 }, false, 'inc'), })), { name: 'prefix' }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() expectTypeOf( useBoundStore.subscribe( (state) => state.count, (count) => console.log(count * 2), ), ).toEqualTypeOf<() => void>() expectTypeOf( useBoundStore.setState({ count: 0 }, false, 'reset'), ).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) it('devtools & persist', () => { const useBoundStore = create()( devtools( persist( (set, get) => ({ count: 1, inc: () => set({ count: get().count + 1 }, false, 'inc'), }), { name: 'count' }, ), { name: 'prefix' }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() expectTypeOf( useBoundStore.setState({ count: 0 }, false, 'reset'), ).toEqualTypeOf() expectTypeOf(useBoundStore.persist.hasHydrated()).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) }) describe('counter state spec (triple middleware)', () => { it('devtools & persist & immer', () => { const useBoundStore = create()( devtools( persist( immer((set, get) => ({ count: 0, inc: () => set((state) => { state.count = get().count + 1 }), })), { name: 'count' }, ), { name: 'prefix' }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() expectTypeOf( useBoundStore.setState({ count: 0 }, false, 'reset'), ).toEqualTypeOf() expectTypeOf(useBoundStore.persist.hasHydrated()).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) it('devtools & subscribeWithSelector & combine', () => { const useBoundStore = create( devtools( subscribeWithSelector( combine({ count: 1 }, (set, get) => ({ inc: () => set({ count: get().count + 1 }, false, 'inc'), })), ), { name: 'prefix' }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() expectTypeOf( useBoundStore.subscribe( (state) => state.count, (count) => console.log(count * 2), ), ).toEqualTypeOf<() => void>() expectTypeOf( useBoundStore.setState({ count: 0 }, false, 'reset'), ).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) it('devtools & subscribeWithSelector & persist', () => { const useBoundStore = create()( devtools( subscribeWithSelector( persist( (set, get) => ({ count: 0, inc: () => set({ count: get().count + 1 }, false), }), { name: 'count' }, ), ), { name: 'prefix' }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() expectTypeOf( useBoundStore.subscribe( (state) => state.count, (count) => console.log(count * 2), ), ).toEqualTypeOf<() => void>() expectTypeOf( useBoundStore.setState({ count: 0 }, false, 'reset'), ).toEqualTypeOf() expectTypeOf(useBoundStore.persist.hasHydrated()).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) }) describe('counter state spec (quadruple middleware)', () => { it('devtools & subscribeWithSelector & persist & immer (#616)', () => { const useBoundStore = create()( devtools( subscribeWithSelector( persist( immer((set, get) => ({ count: 0, inc: () => set((state) => { state.count = get().count + 1 }), })), { name: 'count' }, ), ), { name: 'prefix' }, ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() expectTypeOf( useBoundStore.subscribe( (state) => state.count, (count) => console.log(count * 2), ), ).toEqualTypeOf<() => void>() expectTypeOf( useBoundStore.setState({ count: 0 }, false, 'reset'), ).toEqualTypeOf() expectTypeOf(useBoundStore.persist.hasHydrated()).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) }) describe('more complex state spec with subscribeWithSelector', () => { it('#619, #632', () => { const useBoundStore = create( subscribeWithSelector( persist( () => ({ foo: true, }), { name: 'name' }, ), ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.foo)).toEqualTypeOf() expectTypeOf(useBoundStore().foo).toEqualTypeOf() expectTypeOf(useBoundStore.getState().foo).toEqualTypeOf() useBoundStore.subscribe( (state) => state.foo, (foo) => console.log(foo), ) useBoundStore.persist.hasHydrated() return <> } expect(TestComponent).toBeDefined() }) it('#631', () => { type MyState = { foo: number | null } const useBoundStore = create()( subscribeWithSelector( () => ({ foo: 1, }) as MyState, // NOTE: Asserting the entire state works too. ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.foo)).toEqualTypeOf() expectTypeOf(useBoundStore().foo).toEqualTypeOf() expectTypeOf(useBoundStore.getState().foo).toEqualTypeOf() useBoundStore.subscribe( (state) => state.foo, (foo) => console.log(foo), ) return <> } expect(TestComponent).toBeDefined() }) it('#650', () => { type MyState = { token: string | undefined authenticated: boolean authenticate: (username: string, password: string) => Promise } const useBoundStore = create()( persist( (set) => ({ token: undefined, authenticated: false, authenticate: async (_username, _password) => { set({ authenticated: true }) }, }), { name: 'auth-store' }, ), ) const TestComponent = () => { expectTypeOf( useBoundStore((s) => s.authenticated), ).toEqualTypeOf() expectTypeOf( useBoundStore((s) => s.authenticate)('u', 'p'), ).resolves.toEqualTypeOf() expectTypeOf(useBoundStore().authenticated).toEqualTypeOf() expectTypeOf( useBoundStore().authenticate('u', 'p'), ).resolves.toEqualTypeOf() expectTypeOf( useBoundStore.getState().authenticated, ).toEqualTypeOf() expectTypeOf( useBoundStore.getState().authenticate('u', 'p'), ).resolves.toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) }) describe('create with explicitly annotated mutators', () => { it('subscribeWithSelector & persist', () => { const useBoundStore = create< CounterState, [ ['zustand/subscribeWithSelector', never], ['zustand/persist', CounterState], ] >( subscribeWithSelector( persist( (set, get) => ({ count: 0, inc: () => set({ count: get().count + 1 }, false), }), { name: 'count' }, ), ), ) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.count) * 2).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.inc)()).toEqualTypeOf() expectTypeOf(useBoundStore().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore().inc()).toEqualTypeOf() expectTypeOf(useBoundStore.getState().count * 2).toEqualTypeOf() expectTypeOf(useBoundStore.getState().inc()).toEqualTypeOf() expectTypeOf( useBoundStore.subscribe( (state) => state.count, (count) => console.log(count * 2), ), ).toEqualTypeOf<() => void>() expectTypeOf( useBoundStore.setState({ count: 0 }, false), ).toEqualTypeOf() expectTypeOf(useBoundStore.persist.hasHydrated()).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) }) describe('single middleware with sliced store', () => { it('immer with slices where slice type differs from store (#3371)', () => { interface BearSlice { bears: number addBear: () => void eatFish: () => void } interface FishSlice { fishes: number addFish: () => void } const createBearSlice: StateCreator< BearSlice & FishSlice, [['zustand/immer', never]], [], BearSlice > = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), }) const createFishSlice: StateCreator< BearSlice & FishSlice, [['zustand/immer', never]], [], FishSlice > = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) const useBoundStore = create()((...a) => ({ ...immer(createBearSlice)(...a), ...immer(createFishSlice)(...a), })) const TestComponent = () => { expectTypeOf(useBoundStore((s) => s.bears)).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.fishes)).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.addBear)()).toEqualTypeOf() expectTypeOf(useBoundStore((s) => s.eatFish)()).toEqualTypeOf() return <> } expect(TestComponent).toBeDefined() }) }) ================================================ FILE: tests/persistAsync.test.tsx ================================================ /// import { StrictMode, useEffect } from 'react' import { act, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' import { replacer, reviver, sleep } from './test-utils' const createPersistantStore = (initialValue: string | null) => { let state = initialValue const getItem = async (): Promise => { getItemSpy() await sleep(10) return state } const setItem = async (name: string, newState: string) => { setItemSpy(name, newState) await sleep(10) state = newState } const removeItem = async (name: string) => { removeItemSpy(name) await sleep(10) state = null } const getItemSpy = vi.fn() const setItemSpy = vi.fn() const removeItemSpy = vi.fn() return { storage: { getItem, setItem, removeItem }, getItemSpy, setItemSpy, removeItemSpy, } } describe('persist middleware with async configuration', () => { const consoleError = console.error beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() console.error = consoleError }) it('can rehydrate state', async () => { const onRehydrateStorageSpy = vi.fn() const storage = { getItem: async (name: string) => { await sleep(10) return JSON.stringify({ state: { count: 42, name }, version: 0, }) }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist( () => ({ count: 0, name: 'empty', }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, }, ), ) function Counter() { const { count, name } = useBoundStore() return (
count: {count}, name: {name}
) } render( , ) expect(screen.getByText('count: 0, name: empty')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect( screen.getByText('count: 42, name: test-storage'), ).toBeInTheDocument() expect(onRehydrateStorageSpy).toHaveBeenCalledWith( { count: 42, name: 'test-storage' }, undefined, ) }) it('can throw rehydrate error', async () => { const onRehydrateStorageSpy = vi.fn() const storage = { getItem: async () => { await sleep(10) throw new Error('getItem error') }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, }), ) function Counter() { const { count } = useBoundStore() return
count: {count}
} render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(onRehydrateStorageSpy).toHaveBeenCalledWith( undefined, new Error('getItem error'), ) }) it('can persist state', async () => { const { storage, setItemSpy } = createPersistantStore(null) const createStore = () => { const onRehydrateStorageSpy = vi.fn() const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, }), ) return { useBoundStore, onRehydrateStorageSpy } } // Initialize from empty storage const { useBoundStore, onRehydrateStorageSpy } = createStore() function Counter() { const { count } = useBoundStore() return
count: {count}
} render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(onRehydrateStorageSpy).toHaveBeenCalledWith({ count: 0 }, undefined) // Write something to the store act(() => { useBoundStore.setState({ count: 42 }) }) expect(screen.getByText('count: 42')).toBeInTheDocument() expect(setItemSpy).toHaveBeenCalledWith( 'test-storage', JSON.stringify({ state: { count: 42 }, version: 0 }), ) // Create the same store a second time and check if the persisted state // is loaded correctly const { useBoundStore: useBoundStore2, onRehydrateStorageSpy: onRehydrateStorageSpy2, } = createStore() function Counter2() { const { count } = useBoundStore2() return
count2: {count}
} render( , ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(screen.getByText('count2: 42')).toBeInTheDocument() expect(onRehydrateStorageSpy2).toHaveBeenCalledWith( { count: 42 }, undefined, ) }) it('can async migrate persisted state', async () => { const setItemSpy = vi.fn() const onRehydrateStorageSpy = vi.fn() const migrateSpy = vi.fn(async () => { await sleep(10) return { count: 99 } }) const storage = { getItem: async () => { await sleep(10) return JSON.stringify({ state: { count: 42 }, version: 12, }) }, setItem: setItemSpy, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', version: 13, storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, migrate: migrateSpy, }), ) function Counter() { const { count } = useBoundStore() return
count: {count}
} render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(20)) expect(screen.getByText('count: 99')).toBeInTheDocument() expect(migrateSpy).toHaveBeenCalledWith({ count: 42 }, 12) expect(setItemSpy).toHaveBeenCalledWith( 'test-storage', JSON.stringify({ state: { count: 99 }, version: 13, }), ) expect(onRehydrateStorageSpy).toHaveBeenCalledWith({ count: 99 }, undefined) }) it('can merge partial persisted state', async () => { const storage = { getItem: async () => { await sleep(10) return JSON.stringify({ state: { count: 42 }, }) }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create<{ count: number name: string setName: (name: string) => void }>()( persist( (set) => ({ count: 0, name: 'unknown', setName: (name: string) => { set({ name }) }, }), { name: 'test-storage', storage: createJSONStorage(() => storage), }, ), ) function Component() { const { count, setName, name } = useBoundStore() useEffect(() => { setName('test') }, [setName]) return (
count: {count}
name: {name}
) } render( , ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(screen.getByText('count: 42')).toBeInTheDocument() expect(screen.getByText('name: test')).toBeInTheDocument() expect(useBoundStore.getState()).toEqual( expect.objectContaining({ count: 42, name: 'test', }), ) }) it('can correctly handle a missing migrate function', async () => { console.error = vi.fn() const onRehydrateStorageSpy = vi.fn() const storage = { getItem: async () => { await sleep(10) return JSON.stringify({ state: { count: 42 }, version: 12, }) }, setItem: (_: string, _value: string) => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', version: 13, storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, }), ) function Counter() { const { count } = useBoundStore() return
count: {count}
} render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(console.error).toHaveBeenCalled() expect(onRehydrateStorageSpy).toHaveBeenCalledWith({ count: 0 }, undefined) }) it('can throw migrate error', async () => { console.error = vi.fn() const onRehydrateStorageSpy = vi.fn() const storage = { getItem: async () => { await sleep(10) return JSON.stringify({ state: {}, version: 12, }) }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', version: 13, storage: createJSONStorage(() => storage), migrate: () => { throw new Error('migrate error') }, onRehydrateStorage: () => onRehydrateStorageSpy, }), ) function Counter() { const { count } = useBoundStore() return
count: {count}
} render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(onRehydrateStorageSpy).toHaveBeenCalledWith( undefined, new Error('migrate error'), ) }) it('passes the latest state to onRehydrateStorage and onHydrate on first hydrate', async () => { const onRehydrateStorageSpy = vi.fn() const storage = { getItem: async () => { await sleep(10) return JSON.stringify({ state: { count: 1 } }) }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: onRehydrateStorageSpy, }), ) /** * NOTE: It's currently not possible to add an 'onHydrate' listener which will be * invoked prior to the first hydration. This is because, during first hydration, * the 'onHydrate' listener set (which will be empty) is evaluated before the * 'persist' API is exposed to the caller of 'create'/'createStore'. * * const onHydrateSpy = vi.fn() * useBoundStore.persist.onHydrate(onHydrateSpy) * ... * await act(() => vi.advanceTimersByTimeAsync(10)) * expect(onHydrateSpy).toHaveBeenCalledWith({ count: 0 }) */ function Counter() { const { count } = useBoundStore() return
count: {count}
} render( , ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(screen.getByText('count: 1')).toBeInTheDocument() // The 'onRehydrateStorage' spy is invoked prior to rehydration, so it should // be passed the default state. expect(onRehydrateStorageSpy).toHaveBeenCalledWith({ count: 0 }) }) it('gives the merged state to onRehydrateStorage', async () => { const onRehydrateStorageSpy = vi.fn() const storage = { getItem: async () => { await sleep(10) return JSON.stringify({ state: { count: 1 }, version: 0, }) }, setItem: () => {}, removeItem: () => {}, } const unstorableMethod = () => {} const useBoundStore = create( persist(() => ({ count: 0, unstorableMethod }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, }), ) function Counter() { const { count } = useBoundStore() return
count: {count}
} render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(onRehydrateStorageSpy).toHaveBeenCalledWith( { count: 1, unstorableMethod }, undefined, ) }) it('can custom merge the stored state', async () => { const storage = { getItem: async () => { await sleep(10) return JSON.stringify({ state: { count: 1, actions: {}, }, version: 0, }) }, setItem: () => {}, removeItem: () => {}, } const unstorableMethod = () => {} const useBoundStore = create( persist(() => ({ count: 0, actions: { unstorableMethod } }), { name: 'test-storage', storage: createJSONStorage(() => storage), merge: (_persistedState, currentState) => { const persistedState = _persistedState as any delete persistedState.actions return { ...currentState, ...persistedState, } }, }), ) function Counter() { const { count } = useBoundStore() return
count: {count}
} render( , ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(useBoundStore.getState()).toEqual({ count: 1, actions: { unstorableMethod, }, }) }) it("can merge the state when the storage item doesn't have a version", async () => { const storage = { getItem: async () => { await sleep(10) return JSON.stringify({ state: { count: 1, }, }) }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), }), ) function Counter() { const { count } = useBoundStore() return
count: {count}
} render( , ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(useBoundStore.getState()).toEqual({ count: 1, }) }) it('can manually rehydrate through the api', async () => { const storageValue = '{"state":{"count":1},"version":0}' const storage = { getItem: async () => { await sleep(10) return '' }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), }), ) storage.getItem = async () => { await sleep(10) return storageValue } const rehydratePromise = useBoundStore.persist.rehydrate() await act(() => vi.advanceTimersByTimeAsync(10)) await rehydratePromise expect(useBoundStore.getState()).toEqual({ count: 1, }) }) it('can check if the store has been hydrated through the api', async () => { const storage = { getItem: async () => { await sleep(10) return null }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), }), ) expect(useBoundStore.persist.hasHydrated()).toBe(false) await act(() => vi.advanceTimersByTimeAsync(10)) expect(useBoundStore.persist.hasHydrated()).toBe(true) const rehydratePromise = useBoundStore.persist.rehydrate() await act(() => vi.advanceTimersByTimeAsync(10)) await rehydratePromise expect(useBoundStore.persist.hasHydrated()).toBe(true) }) it('can skip initial hydration', async () => { const storage = { getItem: async (name: string) => { await sleep(10) return { state: { count: 42, name }, version: 0, } }, setItem: () => {}, removeItem: () => {}, } const onRehydrateStorageSpy = vi.fn() const useBoundStore = create( persist( () => ({ count: 0, name: 'empty', }), { name: 'test-storage', storage: storage, onRehydrateStorage: () => onRehydrateStorageSpy, skipHydration: true, }, ), ) expect(useBoundStore.getState()).toEqual({ count: 0, name: 'empty', }) // Asserting store hasn't hydrated expect(useBoundStore.persist.hasHydrated()).toBe(false) const rehydratePromise = useBoundStore.persist.rehydrate() await act(() => vi.advanceTimersByTimeAsync(10)) await rehydratePromise expect(useBoundStore.getState()).toEqual({ count: 42, name: 'test-storage', }) expect(onRehydrateStorageSpy).toHaveBeenCalledWith( { count: 42, name: 'test-storage' }, undefined, ) }) it('handles state updates during onRehydrateStorage', async () => { const storage = { getItem: async () => { await sleep(10) return JSON.stringify({ state: { count: 1 } }) }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create<{ count: number; inc: () => void }>()( persist( (set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })), }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => (s) => s?.inc(), }, ), ) function Counter() { const { count } = useBoundStore() return
count: {count}
} render( , ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(useBoundStore.getState().count).toEqual(2) }) it('passes latest state to post-rehydration callback after hydration-triggered updates', async () => { const onRehydrateStorageSpy = vi.fn() const storage = { getItem: async () => { await sleep(10) return JSON.stringify({ state: { count: 1, bumped: false }, version: 0, }) }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0, bumped: false }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, }), ) let patchedDuringHydration = false const unsubscribe = useBoundStore.subscribe((state) => { if (!patchedDuringHydration && state.count === 1 && !state.bumped) { patchedDuringHydration = true useBoundStore.setState({ bumped: true }) } }) function Counter() { const { count, bumped } = useBoundStore() return (
count: {count}, bumped: {String(bumped)}
) } render( , ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(screen.getByText('count: 1, bumped: true')).toBeInTheDocument() expect(onRehydrateStorageSpy).toHaveBeenCalledWith( { count: 1, bumped: true }, undefined, ) unsubscribe() }) it('can rehydrate state with custom deserialized Map', async () => { const onRehydrateStorageSpy = vi.fn() const storage = { getItem: async () => { await sleep(10) return JSON.stringify({ state: { map: { type: 'Map', value: [['foo', 'bar']] }, }, }) }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist( () => ({ map: new Map(), }), { name: 'test-storage', storage: createJSONStorage(() => storage, { replacer, reviver }), onRehydrateStorage: () => onRehydrateStorageSpy, }, ), ) function MapDisplay() { const { map } = useBoundStore() return
map: {map.get('foo')}
} render( , ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(screen.getByText('map: bar')).toBeInTheDocument() expect(onRehydrateStorageSpy).toHaveBeenCalledWith( { map: new Map([['foo', 'bar']]) }, undefined, ) }) it('can persist state with custom serialization of Map', async () => { const { storage, setItemSpy } = createPersistantStore(null) const map = new Map() const createStore = () => { const onRehydrateStorageSpy = vi.fn() const useBoundStore = create( persist(() => ({ map }), { name: 'test-storage', storage: createJSONStorage(() => storage, { replacer, reviver }), onRehydrateStorage: () => onRehydrateStorageSpy, }), ) return { useBoundStore, onRehydrateStorageSpy } } // Initialize from empty storage const { useBoundStore, onRehydrateStorageSpy } = createStore() function MapDisplay() { const { map } = useBoundStore() return
map-content: {map.get('foo')}
} render( , ) expect(screen.getByText('map-content:')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(onRehydrateStorageSpy).toHaveBeenCalledWith({ map }, undefined) // Write something to the store const updatedMap = new Map(map).set('foo', 'bar') act(() => { useBoundStore.setState({ map: updatedMap }) }) expect(screen.getByText('map-content: bar')).toBeInTheDocument() expect(setItemSpy).toHaveBeenCalledWith( 'test-storage', JSON.stringify({ state: { map: { type: 'Map', value: [['foo', 'bar']] } }, version: 0, }), ) // Create the same store a second time and check if the persisted state // is loaded correctly const { useBoundStore: useBoundStore2, onRehydrateStorageSpy: onRehydrateStorageSpy2, } = createStore() function MapDisplay2() { const { map } = useBoundStore2() return
map-content2: {map.get('foo')}
} render( , ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(screen.getByText('map-content2: bar')).toBeInTheDocument() expect(onRehydrateStorageSpy2).toHaveBeenCalledWith( { map: updatedMap }, undefined, ) }) it('should handle multiple concurrent rehydrate calls (only last one wins)', async () => { let callCount = 0 const storage = { getItem: async () => { const currentCall = ++callCount await sleep(10) return JSON.stringify({ state: { count: currentCall * 10 }, version: 0, }) }, setItem: () => {}, removeItem: () => {}, } const onFinishHydrationSpy = vi.fn() const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), skipHydration: true, }), ) useBoundStore.persist.onFinishHydration(onFinishHydrationSpy) // Start first rehydration const promise1 = useBoundStore.persist.rehydrate() // Immediately start second rehydration (before first completes) const promise2 = useBoundStore.persist.rehydrate() // Start third rehydration const promise3 = useBoundStore.persist.rehydrate() // Advance time to complete all hydrations await act(() => vi.advanceTimersByTimeAsync(30)) await Promise.all([promise1, promise2, promise3]) // Only the last rehydration should have applied its state // callCount will be 3, so count should be 30 expect(useBoundStore.getState().count).toBe(30) // onFinishHydration should only be called once (for the last hydration) expect(onFinishHydrationSpy).toHaveBeenCalledTimes(1) expect(onFinishHydrationSpy).toHaveBeenCalledWith({ count: 30 }) }) it('should not overwrite user state changes made during async hydration', async () => { const storage = { getItem: async () => { await sleep(20) return JSON.stringify({ state: { count: 42, userValue: 'from-storage' }, version: 0, }) }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0, userValue: 'initial' }), { name: 'test-storage', storage: createJSONStorage(() => storage), // Custom merge that preserves user changes made during hydration merge: (persistedState, currentState) => ({ ...currentState, ...(persistedState as object), }), }), ) // User makes a change while hydration is in progress await act(() => vi.advanceTimersByTimeAsync(10)) useBoundStore.setState({ count: 100 }) // Complete hydration await act(() => vi.advanceTimersByTimeAsync(10)) // The merge function combines storage state with current state // Storage has count: 42, userValue: 'from-storage' // Current state before merge has count: 100, userValue: 'initial' // After merge (storage overwrites): count: 42, userValue: 'from-storage' expect(useBoundStore.getState()).toEqual({ count: 42, userValue: 'from-storage', }) }) it('should abort hydration with async migration when newer hydration starts', async () => { let migrationCount = 0 const storage = { getItem: async () => { await sleep(5) return JSON.stringify({ state: { count: 1 }, version: 0, }) }, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), version: 1, migrate: async (state) => { migrationCount++ await sleep(10) return { count: (state as { count: number }).count * 10 } }, skipHydration: true, }), ) // Start first rehydration (will trigger migration) useBoundStore.persist.rehydrate() await act(() => vi.advanceTimersByTimeAsync(5)) // getItem completes // Start second rehydration before migration completes useBoundStore.persist.rehydrate() // Advance time to complete everything await act(() => vi.advanceTimersByTimeAsync(20)) // Both hydrations triggered migration, but only last one should apply expect(migrationCount).toBe(2) // Final state should be from second hydration's migration expect(useBoundStore.getState().count).toBe(10) }) }) ================================================ FILE: tests/persistSync.test.tsx ================================================ /// import { afterEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' import { replacer, reviver } from './test-utils' const createPersistentStore = (initialValue: string | null) => { let state = initialValue const getItem = (): string | null => { getItemSpy() return state } const setItem = (name: string, newState: string) => { setItemSpy(name, newState) state = newState } const removeItem = (name: string) => { removeItemSpy(name) state = null } const getItemSpy = vi.fn() const setItemSpy = vi.fn() const removeItemSpy = vi.fn() return { storage: { getItem, setItem, removeItem }, getItemSpy, setItemSpy, } } describe('persist middleware with sync configuration', () => { const consoleError = console.error afterEach(() => { console.error = consoleError }) it('can rehydrate state', () => { const storage = { getItem: (name: string) => JSON.stringify({ state: { count: 42, name }, version: 0, }), setItem: () => {}, removeItem: () => {}, } const onRehydrateStorageSpy = vi.fn() const useBoundStore = create( persist( () => ({ count: 0, name: 'empty', }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, }, ), ) expect(useBoundStore.getState()).toEqual({ count: 42, name: 'test-storage', }) expect(onRehydrateStorageSpy).toBeCalledWith( { count: 42, name: 'test-storage' }, undefined, ) }) it('can throw rehydrate error', () => { const storage = { getItem: () => { throw new Error('getItem error') }, setItem: () => {}, removeItem: () => {}, } const spy = vi.fn() create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => spy, }), ) expect(spy).toBeCalledWith(undefined, new Error('getItem error')) }) it('can persist state', () => { const { storage, setItemSpy } = createPersistentStore(null) const createStore = () => { const onRehydrateStorageSpy = vi.fn() const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, }), ) return { useBoundStore, onRehydrateStorageSpy } } // Initialize from empty storage const { useBoundStore, onRehydrateStorageSpy } = createStore() expect(useBoundStore.getState()).toEqual({ count: 0 }) expect(onRehydrateStorageSpy).toBeCalledWith({ count: 0 }, undefined) // Write something to the store useBoundStore.setState({ count: 42 }) expect(useBoundStore.getState()).toEqual({ count: 42 }) expect(setItemSpy).toBeCalledWith( 'test-storage', JSON.stringify({ state: { count: 42 }, version: 0 }), ) // Create the same store a second time and check if the persisted state // is loaded correctly const { useBoundStore: useBoundStore2, onRehydrateStorageSpy: onRehydrateStorageSpy2, } = createStore() expect(useBoundStore2.getState()).toEqual({ count: 42 }) expect(onRehydrateStorageSpy2).toBeCalledWith({ count: 42 }, undefined) }) it('can non-async migrate persisted state', () => { const setItemSpy = vi.fn() const onRehydrateStorageSpy = vi.fn() const migrateSpy = vi.fn(() => ({ count: 99 })) const storage = { getItem: () => JSON.stringify({ state: { count: 42 }, version: 12, }), setItem: setItemSpy, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', version: 13, storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, migrate: migrateSpy, }), ) expect(useBoundStore.getState()).toEqual({ count: 99 }) expect(migrateSpy).toBeCalledWith({ count: 42 }, 12) expect(setItemSpy).toBeCalledWith( 'test-storage', JSON.stringify({ state: { count: 99 }, version: 13, }), ) expect(onRehydrateStorageSpy).toBeCalledWith({ count: 99 }, undefined) }) it('can correctly handle a missing migrate function', () => { console.error = vi.fn() const onRehydrateStorageSpy = vi.fn() const storage = { getItem: () => JSON.stringify({ state: { count: 42 }, version: 12, }), setItem: (_: string, _value: string) => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', version: 13, storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, }), ) expect(useBoundStore.getState()).toEqual({ count: 0 }) expect(console.error).toHaveBeenCalled() expect(onRehydrateStorageSpy).toBeCalledWith({ count: 0 }, undefined) }) it('can throw migrate error', () => { const onRehydrateStorageSpy = vi.fn() const storage = { getItem: () => JSON.stringify({ state: {}, version: 12, }), setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', version: 13, storage: createJSONStorage(() => storage), migrate: () => { throw new Error('migrate error') }, onRehydrateStorage: () => onRehydrateStorageSpy, }), ) expect(useBoundStore.getState()).toEqual({ count: 0 }) expect(onRehydrateStorageSpy).toBeCalledWith( undefined, new Error('migrate error'), ) }) it('passes the latest state to onRehydrateStorage and onHydrate on first hydrate', () => { const onRehydrateStorageSpy = vi.fn() const storage = { getItem: () => JSON.stringify({ state: { count: 1 } }), setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: onRehydrateStorageSpy, }), ) /** * NOTE: It's currently not possible to add an 'onHydrate' listener which will be * invoked prior to the first hydration. This is because, during first hydration, * the 'onHydrate' listener set (which will be empty) is evaluated before the * 'persist' API is exposed to the caller of 'create'/'createStore'. * * const onHydrateSpy = vi.fn() * useBoundStore.persist.onHydrate(onHydrateSpy) * expect(onHydrateSpy).toBeCalledWith({ count: 0 }) */ // The 'onRehydrateStorage' and 'onHydrate' spies are invoked prior to rehydration, // so they should both be passed the default state. expect(onRehydrateStorageSpy).toBeCalledWith({ count: 0 }) expect(useBoundStore.getState()).toEqual({ count: 1 }) }) it('gives the merged state to onRehydrateStorage', () => { const onRehydrateStorageSpy = vi.fn() const storage = { getItem: () => JSON.stringify({ state: { count: 1 }, version: 0, }), setItem: () => {}, removeItem: () => {}, } const unstorableMethod = () => {} const useBoundStore = create( persist(() => ({ count: 0, unstorableMethod }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, }), ) const expectedState = { count: 1, unstorableMethod } expect(useBoundStore.getState()).toEqual(expectedState) expect(onRehydrateStorageSpy).toBeCalledWith(expectedState, undefined) }) it('can custom merge the stored state', () => { const storage = { getItem: () => JSON.stringify({ state: { count: 1, actions: {}, }, version: 0, }), setItem: () => {}, removeItem: () => {}, } const unstorableMethod = () => {} const useBoundStore = create( persist(() => ({ count: 0, actions: { unstorableMethod } }), { name: 'test-storage', storage: createJSONStorage(() => storage), merge: (_persistedState, currentState) => { const persistedState = _persistedState as any delete persistedState.actions return { ...currentState, ...persistedState, } }, }), ) expect(useBoundStore.getState()).toEqual({ count: 1, actions: { unstorableMethod, }, }) }) it("can merge the state when the storage item doesn't have a version", () => { const storage = { getItem: () => JSON.stringify({ state: { count: 1, }, }), setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), }), ) expect(useBoundStore.getState()).toEqual({ count: 1, }) }) it('can filter the persisted value', () => { const setItemSpy = vi.fn() const storage = { getItem: () => '', setItem: setItemSpy, removeItem: () => {}, } const useBoundStore = create( persist( () => ({ object: { first: '0', second: '1', }, array: [ { value: '0', }, { value: '1', }, { value: '2', }, ], }), { name: 'test-storage', storage: createJSONStorage(() => storage), partialize: (state) => { return { object: { first: state.object.first, }, array: state.array.filter((e) => e.value !== '1'), } }, }, ), ) useBoundStore.setState({}) expect(setItemSpy).toBeCalledWith( 'test-storage', JSON.stringify({ state: { object: { first: '0', }, array: [ { value: '0', }, { value: '2', }, ], }, version: 0, }), ) }) it('can access the options through the api', () => { const storage = { getItem: () => null, setItem: vi.fn(), removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), }), ) expect(useBoundStore.persist.getOptions().name).toBeDefined() expect(useBoundStore.persist.getOptions().name).toBe('test-storage') }) it('can change the options through the api', () => { const setItemSpy = vi.fn() const storage = { getItem: () => null, setItem: setItemSpy, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), partialize: (s) => s as Partial, }), ) useBoundStore.setState({}) expect(setItemSpy).toBeCalledWith( 'test-storage', '{"state":{"count":0},"version":0}', ) useBoundStore.persist.setOptions({ name: 'test-storage-2', partialize: (state) => Object.fromEntries( Object.entries(state).filter(([key]) => key !== 'count'), ), }) useBoundStore.setState({}) expect(setItemSpy).toBeCalledWith( 'test-storage-2', '{"state":{},"version":0}', ) }) it('can clear the storage through the api', () => { const removeItemSpy = vi.fn() const storage = { getItem: () => null, setItem: () => {}, removeItem: removeItemSpy, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), }), ) useBoundStore.persist.clearStorage() expect(removeItemSpy).toBeCalledWith('test-storage') }) it('can manually rehydrate through the api', () => { const storageValue = '{"state":{"count":1},"version":0}' const storage = { getItem: () => '', setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), }), ) storage.getItem = () => storageValue useBoundStore.persist.rehydrate() expect(useBoundStore.getState()).toEqual({ count: 1, }) }) it('can check if the store has been hydrated through the api', async () => { const storage = { getItem: () => null, setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), }), ) expect(useBoundStore.persist.hasHydrated()).toBe(true) await useBoundStore.persist.rehydrate() expect(useBoundStore.persist.hasHydrated()).toBe(true) }) it('can wait for rehydration through the api', async () => { const storageValue1 = '{"state":{"count":1},"version":0}' const storageValue2 = '{"state":{"count":2},"version":0}' const onHydrateSpy1 = vi.fn() const onHydrateSpy2 = vi.fn() const onFinishHydrationSpy1 = vi.fn() const onFinishHydrationSpy2 = vi.fn() const storage = { getItem: () => '', setItem: () => {}, removeItem: () => {}, } const useBoundStore = create( persist(() => ({ count: 0 }), { name: 'test-storage', storage: createJSONStorage(() => storage), }), ) const hydrateUnsub1 = useBoundStore.persist.onHydrate(onHydrateSpy1) useBoundStore.persist.onHydrate(onHydrateSpy2) const finishHydrationUnsub1 = useBoundStore.persist.onFinishHydration( onFinishHydrationSpy1, ) useBoundStore.persist.onFinishHydration(onFinishHydrationSpy2) storage.getItem = () => storageValue1 await useBoundStore.persist.rehydrate() expect(onHydrateSpy1).toBeCalledWith({ count: 0 }) expect(onHydrateSpy2).toBeCalledWith({ count: 0 }) expect(onFinishHydrationSpy1).toBeCalledWith({ count: 1 }) expect(onFinishHydrationSpy2).toBeCalledWith({ count: 1 }) hydrateUnsub1() finishHydrationUnsub1() storage.getItem = () => storageValue2 await useBoundStore.persist.rehydrate() expect(onHydrateSpy1).not.toBeCalledTimes(2) expect(onHydrateSpy2).toBeCalledWith({ count: 1 }) expect(onFinishHydrationSpy1).not.toBeCalledTimes(2) expect(onFinishHydrationSpy2).toBeCalledWith({ count: 2 }) }) it('can skip initial hydration', async () => { const storage = { getItem: (name: string) => ({ state: { count: 42, name }, version: 0, }), setItem: () => {}, removeItem: () => {}, } const onRehydrateStorageSpy = vi.fn() const useBoundStore = create( persist( () => ({ count: 0, name: 'empty', }), { name: 'test-storage', storage: storage, onRehydrateStorage: () => onRehydrateStorageSpy, skipHydration: true, }, ), ) expect(useBoundStore.getState()).toEqual({ count: 0, name: 'empty', }) // Asserting store hasn't hydrated expect(useBoundStore.persist.hasHydrated()).toBe(false) await useBoundStore.persist.rehydrate() expect(useBoundStore.getState()).toEqual({ count: 42, name: 'test-storage', }) expect(onRehydrateStorageSpy).toHaveBeenCalledWith( { count: 42, name: 'test-storage' }, undefined, ) }) it('handles state updates during onRehydrateStorage', () => { const storage = { getItem: () => JSON.stringify({ state: { count: 1 } }), setItem: () => {}, removeItem: () => {}, } const useBoundStore = create<{ count: number; inc: () => void }>()( persist( (set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })), }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => (s) => s?.inc(), }, ), ) expect(useBoundStore.getState().count).toEqual(2) }) it('can rehydrate state with custom deserialized Map', () => { const storage = { getItem: () => JSON.stringify({ map: { type: 'Map', value: [['foo', 'bar']] }, }), setItem: () => {}, removeItem: () => {}, } const map = new Map() const onRehydrateStorageSpy = vi.fn() const useBoundStore = create( persist( () => ({ map, }), { name: 'test-storage', storage: createJSONStorage(() => storage), onRehydrateStorage: () => onRehydrateStorageSpy, }, ), ) const updatedMap = map.set('foo', 'bar') expect(useBoundStore.getState()).toEqual({ map: updatedMap, }) expect(onRehydrateStorageSpy).toBeCalledWith({ map: updatedMap }, undefined) }) it('can persist state with custom serialization of Map', () => { const { storage, setItemSpy } = createPersistentStore(null) const map = new Map() const createStore = () => { const onRehydrateStorageSpy = vi.fn() const useBoundStore = create( persist(() => ({ map }), { name: 'test-storage', storage: createJSONStorage(() => storage, { replacer, reviver }), onRehydrateStorage: () => onRehydrateStorageSpy, }), ) return { useBoundStore, onRehydrateStorageSpy } } // Initialize from empty storage const { useBoundStore, onRehydrateStorageSpy } = createStore() expect(useBoundStore.getState()).toEqual({ map }) expect(onRehydrateStorageSpy).toBeCalledWith({ map }, undefined) // Write something to the store const updatedMap = map.set('foo', 'bar') useBoundStore.setState({ map: updatedMap }) expect(useBoundStore.getState()).toEqual({ map: updatedMap, }) expect(setItemSpy).toBeCalledWith( 'test-storage', JSON.stringify({ state: { map: { type: 'Map', value: [['foo', 'bar']] } }, version: 0, }), ) // Create the same store a second time and check if the persisted state // is loaded correctly const { useBoundStore: useBoundStore2, onRehydrateStorageSpy: onRehydrateStorageSpy2, } = createStore() expect(useBoundStore2.getState()).toEqual({ map: updatedMap }) expect(onRehydrateStorageSpy2).toBeCalledWith( { map: updatedMap }, undefined, ) }) it('does not call setItem when hydrating from its own storage', async () => { const setItem = vi.fn() const storage = { getItem: (name: string) => ({ state: { count: 42, name }, version: 0, }), setItem, removeItem: () => {}, } const useBoundStore = create( persist(() => ({}), { name: 'test-storage', storage: storage, }), ) expect(useBoundStore.persist.hasHydrated()).toBe(true) expect(setItem).toBeCalledTimes(0) }) }) ================================================ FILE: tests/setup.ts ================================================ import '@testing-library/jest-dom/vitest' ================================================ FILE: tests/shallow.test.tsx ================================================ import { useState } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import { shallow, useShallow } from 'zustand/shallow' import { createWithEqualityFn } from 'zustand/traditional' describe('types', () => { it('works with useBoundStore and array selector (#1107)', () => { const useBoundStore = createWithEqualityFn(() => ({ villages: [] as { name: string }[], })) const Component = () => { const villages = useBoundStore((state) => state.villages, shallow) return <>{villages.length} } expect(Component).toBeDefined() }) it('works with useBoundStore and string selector (#1107)', () => { const useBoundStore = createWithEqualityFn(() => ({ refetchTimestamp: '', })) const Component = () => { const refetchTimestamp = useBoundStore( (state) => state.refetchTimestamp, shallow, ) return <>{refetchTimestamp.toUpperCase()} } expect(Component).toBeDefined() }) }) describe('useShallow', () => { const testUseShallowSimpleCallback = vi.fn() const TestUseShallowSimple = ({ selector, state, }: { state: Record selector: (state: Record) => string[] }) => { const selectorOutput = selector(state) const useShallowOutput = useShallow(selector)(state) return (
testUseShallowSimpleCallback({ selectorOutput, useShallowOutput }) } /> ) } beforeEach(() => { testUseShallowSimpleCallback.mockClear() }) it('input and output selectors always return shallow equal values', () => { const { rerender } = render( , ) expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(0) fireEvent.click(screen.getByTestId('test-shallow')) const firstRender = testUseShallowSimpleCallback.mock.lastCall?.[0] expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(1) expect(firstRender).toBeTruthy() expect(firstRender?.selectorOutput).toEqual(firstRender?.useShallowOutput) rerender( , ) fireEvent.click(screen.getByTestId('test-shallow')) expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(2) const secondRender = testUseShallowSimpleCallback.mock.lastCall?.[0] expect(secondRender).toBeTruthy() expect(secondRender?.selectorOutput).toEqual(secondRender?.useShallowOutput) }) it('returns the previously computed instance when possible', () => { const state = { a: 1, b: 2 } const { rerender } = render( , ) fireEvent.click(screen.getByTestId('test-shallow')) expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(1) const output1 = testUseShallowSimpleCallback.mock.lastCall?.[0]?.useShallowOutput expect(output1).toBeTruthy() // Change selector, same output rerender( Object.keys(state)} />, ) fireEvent.click(screen.getByTestId('test-shallow')) expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(2) const output2 = testUseShallowSimpleCallback.mock.lastCall?.[0]?.useShallowOutput expect(output2).toBeTruthy() expect(output2).toBe(output1) }) it('only re-renders if selector output has changed according to shallow', () => { let countRenders = 0 const useMyStore = create( (): Record => ({ a: 1, b: 2, c: 3 }), ) const TestShallow = ({ selector = (state) => Object.keys(state).sort(), }: { selector?: (state: Record) => string[] }) => { const output = useMyStore(useShallow(selector)) ++countRenders return
{output.join(',')}
} expect(countRenders).toBe(0) render() expect(countRenders).toBe(1) expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c') act(() => { useMyStore.setState({ a: 4 }) // This will not cause a re-render. }) expect(countRenders).toBe(1) act(() => { useMyStore.setState({ d: 10 }) // This will cause a re-render. }) expect(countRenders).toBe(2) expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c,d') }) it('does not cause stale closure issues', () => { const useMyStore = create( (): Record => ({ a: 1, b: 2, c: 3 }), ) const TestShallowWithState = () => { const [count, setCount] = useState(0) const output = useMyStore( useShallow((state) => Object.keys(state).concat([count.toString()])), ) return (
setCount((prev) => ++prev)} > {output.join(',')}
) } render() expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c,0') fireEvent.click(screen.getByTestId('test-shallow')) expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c,1') }) }) ================================================ FILE: tests/ssr.test.tsx ================================================ import React, { useEffect } from 'react' import { act, screen } from '@testing-library/react' import { renderToString } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' import { create } from 'zustand' interface BearStoreState { bears: number } interface BearStoreAction { increasePopulation: () => void } const initialState = { bears: 0 } const useBearStore = create((set) => ({ ...initialState, increasePopulation: () => set(({ bears }) => ({ bears: bears + 1 })), })) function Counter() { const bears = useBearStore(({ bears }) => bears) const increasePopulation = useBearStore( ({ increasePopulation }) => increasePopulation, ) useEffect(() => { increasePopulation() }, [increasePopulation]) return
bears: {bears}
} describe('ssr behavior with react 18+', () => { it('should handle different states between server and client correctly', async () => { const { hydrateRoot } = await vi.importActual( 'react-dom/client', ) const view = renderToString( Loading...
}> , ) const container = document.createElement('div') document.body.appendChild(container) container.innerHTML = view expect(container).toHaveTextContent(/bears: 0/) await act(async () => { hydrateRoot( container, Loading...}> , ) }) expect(screen.getByText('bears: 1')).toBeInTheDocument() document.body.removeChild(container) }) it('should not have hydration errors', async () => { const useStore = create(() => ({ bears: 0, })) const { hydrateRoot } = await vi.importActual( 'react-dom/client', ) const Component = () => { const bears = useStore((state) => state.bears) return
bears: {bears}
} const view = renderToString( Loading...}> , ) const container = document.createElement('div') document.body.appendChild(container) container.innerHTML = view expect(container).toHaveTextContent(/bears: 0/) const consoleMock = vi.spyOn(console, 'error') const hydratePromise = act(async () => { hydrateRoot( container, Loading...}> , ) }) // set state during hydration useStore.setState({ bears: 1 }) await hydratePromise expect(consoleMock).toHaveBeenCalledTimes(0) expect(screen.getByText('bears: 1')).toBeInTheDocument() document.body.removeChild(container) }) }) ================================================ FILE: tests/subscribe.test.tsx ================================================ import { describe, expect, it } from 'vitest' import { create } from 'zustand' describe('subscribe()', () => { it('should correctly have access to subscribe', () => { const { subscribe } = create(() => ({ value: 1 })) expect(typeof subscribe).toBe('function') }) }) ================================================ FILE: tests/test-utils.ts ================================================ type ReplacedMap = { type: 'Map' value: [string, unknown][] } export const replacer = ( key: string, value: unknown, ): ReplacedMap | unknown => { if (value instanceof Map) { return { type: 'Map', value: Array.from(value.entries()), } } else { return value } } export const reviver = (key: string, value: ReplacedMap | unknown): unknown => { if (isReplacedMap(value)) { return new Map(value.value) } return value } const isReplacedMap = (value: any): value is ReplacedMap => { if (value && value.type === 'Map') { return true } return false } export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } ================================================ FILE: tests/types.test.tsx ================================================ import { expect, it } from 'vitest' import { create } from 'zustand' import type { StateCreator, StoreApi, StoreMutatorIdentifier, UseBoundStore, } from 'zustand' import { persist } from 'zustand/middleware' it('can use exposed types', () => { type ExampleState = { num: number numGet: () => number numGetState: () => number numSet: (v: number) => void numSetState: (v: number) => void } const listener = (state: ExampleState) => { if (state) { const value = state.num * state.numGet() * state.numGetState() state.numSet(value) state.numSetState(value) } } const selector = (state: ExampleState) => state.num const partial: Partial = { num: 2, numGet: () => 2, } const partialFn: (state: ExampleState) => Partial = ( state, ) => ({ ...state, num: 2, }) const equalityFn = (state: ExampleState, newState: ExampleState) => state !== newState const storeApi = create((set, get) => ({ num: 1, numGet: () => get().num, numGetState: () => { // TypeScript can't get the type of storeApi when it tries to enforce the signature of numGetState. // Need to explicitly state the type of storeApi.getState().num or storeApi type will be type 'any'. const result: number = storeApi.getState().num return result }, numSet: (v) => { set({ num: v }) }, numSetState: (v) => { storeApi.setState({ num: v }) }, })) const useBoundStore = storeApi const stateCreator: StateCreator = (set, get) => ({ num: 1, numGet: () => get().num, numGetState: () => get().num, numSet: (v) => { set({ num: v }) }, numSetState: (v) => { set({ num: v }) }, }) function checkAllTypes( _getState: StoreApi['getState'], _partialState: | Partial | ((s: ExampleState) => Partial), _setState: StoreApi['setState'], _state: object, _stateListener: (state: ExampleState, previousState: ExampleState) => void, _stateSelector: (state: ExampleState) => number, _storeApi: StoreApi, _subscribe: StoreApi['subscribe'], _equalityFn: (a: ExampleState, b: ExampleState) => boolean, _stateCreator: StateCreator, _useBoundStore: UseBoundStore>, ) { expect(true).toBeTruthy() } checkAllTypes( storeApi.getState, Math.random() > 0.5 ? partial : partialFn, storeApi.setState, storeApi.getState(), listener, selector, storeApi, storeApi.subscribe, equalityFn, stateCreator, useBoundStore, ) }) type AssertEqual = Type extends Expected ? Expected extends Type ? true : never : never it('should have correct (partial) types for setState', () => { type Count = { count: number } const store = create((set) => ({ count: 0, // @ts-expect-error we shouldn't be able to set count to undefined a: () => set(() => ({ count: undefined })), // @ts-expect-error we shouldn't be able to set count to undefined b: () => set({ count: undefined }), c: () => set({ count: 1 }), })) const setState: AssertEqual< typeof store.setState, StoreApi['setState'] > = true expect(setState).toEqual(true) // ok, should not error store.setState({ count: 1 }) store.setState({}) store.setState((previous) => previous) // @ts-expect-error type undefined is not assignable to type number store.setState({ count: undefined }) // @ts-expect-error type undefined is not assignable to type number store.setState((state) => ({ ...state, count: undefined })) }) it('should allow for different partial keys to be returnable from setState', () => { type State = { count: number something: string } const store = create(() => ({ count: 0, something: 'foo', })) const setState: AssertEqual< typeof store.setState, StoreApi['setState'] > = true expect(setState).toEqual(true) // ok, should not error store.setState((previous) => { if (previous.count === 0) { return { count: 1 } } return { count: 0 } }) store.setState((previous) => { if (previous.count === 0) { return { count: 1 } } if (previous.count === 1) { return previous } return { something: 'foo' } }) // @ts-expect-error Type '{ something: boolean; count?: undefined; }' is not assignable to type 'State'. store.setState((previous) => { if (previous.count === 0) { return { count: 1 } } return { something: true } }) }) it('state is covariant', () => { const store = create<{ count: number; foo: string }>()(() => ({ count: 0, foo: '', })) const testIsCovariant: StoreApi<{ count: number }> = store expect(testIsCovariant).toBeDefined() // @ts-expect-error should not compile const testIsNotContravariant: StoreApi<{ count: number foo: string baz: string }> = store expect(testIsNotContravariant).toBeDefined() }) it('StateCreator is StateCreator', () => { interface State { count: number increment: () => void } const foo: () => StateCreator< State, M > = () => (set, get) => ({ count: 0, increment: () => { set({ count: get().count + 1 }) }, }) const store = create()(persist(foo(), { name: 'prefix' })) expect(store).toBeDefined() }) it('StateCreator subtyping', () => { interface State { count: number increment: () => void } const foo: () => StateCreator = () => (set, get) => ({ count: 0, increment: () => { set({ count: get().count + 1 }) }, }) create()(persist(foo(), { name: 'prefix' })) const testSubtyping: StateCreator = {} as StateCreator expect(testSubtyping).toBeDefined() }) it('set state exists on store with readonly store', () => { interface State { count: number increment: () => void } const useStore = create()((set, get) => ({ count: 0, increment: () => set({ count: get().count + 1 }), })) useStore.setState((state) => ({ ...state, count: state.count + 1 })) expect(useStore).toBeDefined() }) ================================================ FILE: tests/vanilla/basic.test.ts ================================================ import { afterEach, expect, it, vi } from 'vitest' import { createStore } from 'zustand/vanilla' import type { StoreApi } from 'zustand/vanilla' // To avoid include react deps on vanilla version vi.mock('react', () => ({})) const consoleError = console.error afterEach(() => { console.error = consoleError }) it('create a store', () => { let params const result = createStore((...args) => { params = args return { value: null } }) expect({ params, result }).toMatchInlineSnapshot(` { "params": [ [Function], [Function], { "getInitialState": [Function], "getState": [Function], "setState": [Function], "subscribe": [Function], }, ], "result": { "getInitialState": [Function], "getState": [Function], "setState": [Function], "subscribe": [Function], }, } `) }) type CounterState = { count: number inc: () => void } it('uses the store', async () => { const store = createStore((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) store.getState().inc() expect(store.getState().count).toBe(1) }) it('can get the store', async () => { type State = { value: number getState1: () => State getState2: () => State } const store = createStore((_, get) => ({ value: 1, getState1: () => get(), getState2: (): State => store.getState(), })) expect(store.getState().getState1().value).toBe(1) expect(store.getState().getState2().value).toBe(1) }) it('can get the initial state', () => { const initial = { value: 1 } const store = createStore(() => initial) store.setState({ value: 2 }) expect(store.getInitialState()).toBe(initial) }) it('can set the store', async () => { type State = { value: number setState1: StoreApi['setState'] setState2: StoreApi['setState'] } const store = createStore((set) => ({ value: 1, setState1: (v) => set(v), setState2: (v): void => store.setState(v), })) store.getState().setState1({ value: 2 }) expect(store.getState().value).toBe(2) store.getState().setState2({ value: 3 }) expect(store.getState().value).toBe(3) }) it('both NaN should not update', () => { const store = createStore(() => NaN) const fn = vi.fn() store.subscribe(fn) store.setState(NaN) expect(fn).not.toBeCalled() }) it('can set the store without merging', () => { const { setState, getState } = createStore<{ a: number } | { b: number }>( (_set) => ({ a: 1, }), ) // Should override the state instead of merging. setState({ b: 2 }, true) expect(getState()).toEqual({ b: 2 }) }) it('can set the object store to null', () => { const { setState, getState } = createStore<{ a: number } | null>(() => ({ a: 1, })) setState(null) expect(getState()).toEqual(null) }) it('can set the non-object store to null', () => { const { setState, getState } = createStore(() => 'value') setState(null) expect(getState()).toEqual(null) }) it('works with non-object state', () => { const store = createStore(() => 1) const inc = () => store.setState((c) => c + 1) inc() expect(store.getState()).toBe(2) }) ================================================ FILE: tests/vanilla/shallow.test.tsx ================================================ import { describe, expect, it } from 'vitest' import { shallow } from 'zustand/shallow' describe('shallow', () => { it('compares primitive values', () => { expect(shallow(true, true)).toBe(true) expect(shallow(true, false)).toBe(false) expect(shallow(1, 1)).toBe(true) expect(shallow(1, 2)).toBe(false) expect(shallow('zustand', 'zustand')).toBe(true) expect(shallow('zustand', 'redux')).toBe(false) }) it('compares objects', () => { expect(shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', asd: 123 })).toBe( true, ) expect( shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', foobar: true }), ).toBe(false) expect( shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', asd: 123, foobar: true }), ).toBe(false) }) it('compares arrays', () => { expect(shallow([1, 2, 3], [1, 2, 3])).toBe(true) expect(shallow([1, 2, 3], [2, 3, 4])).toBe(false) expect( shallow([{ foo: 'bar' }, { asd: 123 }], [{ foo: 'bar' }, { asd: 123 }]), ).toBe(false) expect(shallow([{ foo: 'bar' }], [{ foo: 'bar', asd: 123 }])).toBe(false) expect(shallow([1, 2, 3], [2, 3, 1])).toBe(false) }) it('compares Maps', () => { expect( shallow( new Map([ ['foo', 'bar'], ['asd', 123], ]), new Map([ ['foo', 'bar'], ['asd', 123], ]), ), ).toBe(true) expect( shallow( new Map([ ['foo', 'bar'], ['asd', 123], ]), new Map([ ['asd', 123], ['foo', 'bar'], ]), ), ).toBe(true) expect( shallow( new Map([ ['foo', 'bar'], ['asd', 123], ]), new Map([ ['foo', 'bar'], ['foobar', true], ]), ), ).toBe(false) expect( shallow( new Map([ ['foo', 'bar'], ['asd', 123], ]), new Map([ ['foo', 'bar'], ['asd', 123], ['foobar', true], ]), ), ).toBe(false) const obj = {} const obj2 = {} expect( shallow( new Map([[obj, 'foo']]), new Map([[obj2, 'foo']]), ), ).toBe(false) }) it('compares Sets', () => { expect(shallow(new Set(['bar', 123]), new Set(['bar', 123]))).toBe(true) expect(shallow(new Set(['bar', 123]), new Set([123, 'bar']))).toBe(true) expect(shallow(new Set(['bar', 123]), new Set(['bar', 2]))).toBe(false) expect(shallow(new Set(['bar', 123]), new Set(['bar', 123, true]))).toBe( false, ) const obj = {} const obj2 = {} expect(shallow(new Set([obj]), new Set([obj]))).toBe(true) expect(shallow(new Set([obj]), new Set([obj2]))).toBe(false) expect(shallow(new Set([obj]), new Set([obj, obj2]))).toBe(false) expect(shallow(new Set([obj]), new Set([obj2, obj]))).toBe(false) expect(shallow(['bar', 123] as never, new Set(['bar', 123]))).toBe(false) }) it('compares functions', () => { function firstFnCompare() { return { foo: 'bar' } } function secondFnCompare() { return { foo: 'bar' } } expect(shallow(firstFnCompare, firstFnCompare)).toBe(true) expect(shallow(secondFnCompare, secondFnCompare)).toBe(true) expect(shallow(firstFnCompare, secondFnCompare)).toBe(false) }) it('compares URLSearchParams', () => { expect( shallow(new URLSearchParams({ a: 'a' }), new URLSearchParams({ a: 'a' })), ).toBe(true) expect( shallow(new URLSearchParams({ a: 'a' }), new URLSearchParams({ a: 'b' })), ).toBe(false) expect( shallow(new URLSearchParams({ a: 'a' }), new URLSearchParams({ b: 'b' })), ).toBe(false) expect( shallow( new URLSearchParams({ a: 'a' }), new URLSearchParams({ a: 'a', b: 'b' }), ), ).toBe(false) expect( shallow( new URLSearchParams({ b: 'b', a: 'a' }), new URLSearchParams({ a: 'a', b: 'b' }), ), ).toBe(true) }) it('should work with nested arrays (#2794)', () => { const arr = [1, 2] expect(shallow([arr, 1], [arr, 1])).toBe(true) }) it('should work with undefined (#3204)', () => { expect(shallow({ a: undefined }, { b: 1 })).toBe(false) }) }) describe('mixed cases', () => { const obj = { 0: 'foo', 1: 'bar' } const arr = ['foo', 'bar'] const set = new Set(['foo', 'bar']) const map = new Map([ [0, 'foo'], [1, 'bar'], ]) it('compares different data structures', () => { expect(shallow(obj, arr)).toBe(false) expect(shallow(obj, set)).toBe(false) expect(shallow(obj, map)).toBe(false) expect(shallow(arr, set)).toBe(false) expect(shallow(arr, map)).toBe(false) expect(shallow(set, map)).toBe(false) }) }) describe('generators', () => { it('pure iterable', () => { function* gen() { yield 1 yield 2 } expect(Symbol.iterator in gen()).toBe(true) expect(shallow(gen(), gen())).toBe(true) }) it('pure iterable with different values returns false', () => { const iterableA = { [Symbol.iterator]: function* (): Generator { yield 1 yield 2 }, } const iterableB = { [Symbol.iterator]: function* (): Generator { yield 1 yield 3 }, } expect(shallow(iterableA, iterableB)).toBe(false) }) }) describe('unsupported cases', () => { it('date', () => { expect( shallow( new Date('2022-07-19T00:00:00.000Z'), new Date('2022-07-20T00:00:00.000Z'), ), ).not.toBe(false) }) }) ================================================ FILE: tests/vanilla/subscribe.test.tsx ================================================ import { describe, expect, it, vi } from 'vitest' import { subscribeWithSelector } from 'zustand/middleware' import { createStore } from 'zustand/vanilla' describe('subscribe()', () => { it('should not be called if new state identity is the same', () => { const spy = vi.fn() const initialState = { value: 1, other: 'a' } const { setState, subscribe } = createStore(() => initialState) subscribe(spy) setState(initialState) expect(spy).not.toHaveBeenCalled() }) it('should be called if new state identity is different', () => { const spy = vi.fn() const initialState = { value: 1, other: 'a' } const { setState, getState, subscribe } = createStore(() => initialState) subscribe(spy) setState({ ...getState() }) expect(spy).toHaveBeenCalledWith(initialState, initialState) }) it('should not be called when state slice is the same', () => { const spy = vi.fn() const initialState = { value: 1, other: 'a' } const { setState, subscribe } = createStore( subscribeWithSelector(() => initialState), ) subscribe((s) => s.value, spy) setState({ other: 'b' }) expect(spy).not.toHaveBeenCalled() }) it('should be called when state slice changes', () => { const spy = vi.fn() const initialState = { value: 1, other: 'a' } const { setState, subscribe } = createStore( subscribeWithSelector(() => initialState), ) subscribe((s) => s.value, spy) setState({ value: initialState.value + 1 }) expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value) }) it('should not be called when equality checker returns true', () => { const spy = vi.fn() const initialState = { value: 1, other: 'a' } const { setState, subscribe } = createStore( subscribeWithSelector(() => initialState), ) subscribe((s) => s, spy, { equalityFn: () => true }) setState({ value: initialState.value + 2 }) expect(spy).not.toHaveBeenCalled() }) it('should be called when equality checker returns false', () => { const spy = vi.fn() const initialState = { value: 1, other: 'a' } const { setState, subscribe } = createStore( subscribeWithSelector(() => initialState), ) subscribe((s) => s.value, spy, { equalityFn: () => false }) setState({ value: initialState.value + 2 }) expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledWith(initialState.value + 2, initialState.value) }) it('should unsubscribe correctly', () => { const spy = vi.fn() const initialState = { value: 1, other: 'a' } const { setState, subscribe } = createStore( subscribeWithSelector(() => initialState), ) const unsub = subscribe((s) => s.value, spy) setState({ value: initialState.value + 1 }) unsub() setState({ value: initialState.value + 2 }) expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value) }) it('should keep consistent behavior with equality check', () => { const spy = vi.fn() const initialState = { value: 1, other: 'a' } const { getState, setState, subscribe } = createStore( subscribeWithSelector(() => initialState), ) const isRoughEqual = (x: number, y: number) => Math.abs(x - y) < 1 setState({ value: 0 }) spy.mockReset() const spy2 = vi.fn() let prevValue = getState().value const unsub = subscribe((s) => { if (isRoughEqual(prevValue, s.value)) { // skip assuming values are equal return } spy(s.value, prevValue) prevValue = s.value }) const unsub2 = subscribe((s) => s.value, spy2, { equalityFn: isRoughEqual }) setState({ value: 0.5 }) setState({ value: 1 }) unsub() unsub2() expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledWith(1, 0) expect(spy2).toHaveBeenCalledTimes(1) expect(spy2).toHaveBeenCalledWith(1, 0) }) it('should call listener immediately when fireImmediately is true', () => { const spy = vi.fn() const initialState = { value: 1 } const { subscribe } = createStore(subscribeWithSelector(() => initialState)) subscribe((s) => s.value, spy, { fireImmediately: true }) expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledWith(1, 1) }) }) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "strict": true, "jsx": "react-jsx", "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "skipLibCheck": true /* FIXME remove this once redux-devtools/extension fixes it */, "allowImportingTsExtensions": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "verbatimModuleSyntax": true, "declaration": true, "isolatedDeclarations": true, "types": ["@testing-library/jest-dom/vitest"], "noEmit": true, "baseUrl": ".", "paths": { "zustand": ["./src/index.ts"], "zustand/*": ["./src/*.ts"] } }, "include": ["src/**/*", "tests/**/*"], "exclude": ["node_modules", "dist"] } ================================================ FILE: vitest.config.mts ================================================ import { resolve } from 'path' import { defineConfig } from 'vitest/config' export default defineConfig({ resolve: { alias: [ { find: /^zustand$/, replacement: resolve('./src/index.ts') }, { find: /^zustand(.*)$/, replacement: resolve('./src/$1.ts') }, ], }, test: { name: 'zustand', // Keeping globals to true triggers React Testing Library's auto cleanup // https://vitest.dev/guide/migration.html globals: true, environment: 'jsdom', dir: 'tests', reporters: process.env.GITHUB_ACTIONS ? ['default', 'github-actions'] : ['default'], setupFiles: ['tests/setup.ts'], coverage: { include: ['src/**/'], reporter: ['text', 'json', 'html', 'text-summary'], reportsDirectory: './coverage/', provider: 'v8', }, }, })