Showing preview only (719K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<p align="center">
<img src="./docs/bear.jpg" />
</p>
[](https://github.com/pmndrs/zustand/actions?query=workflow%3ATest)
[](https://bundlejs.com/?q=zustand)
[](https://www.npmjs.com/package/zustand)
[](https://www.npmjs.com/package/zustand)
[](https://discord.gg/poimandres)
<a href="https://dai-shi.github.io/zustand-banner-sponsorship/sponsors/" target="_blank" rel="noopener">
<p align="center">
<img src="https://dai-shi.github.io/zustand-banner-sponsorship/api/banner.png" />
</p>
</a>
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 <h1>{bears} around here ...</h1>
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
```
### 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 = () => (
<StoreContext.Provider value={store}>
...
</StoreContext.Provider>
)
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<State>()(...)` 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<BearState>()(
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<State & Actions>((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<State & Actions>((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<State & Actions>((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<typeof countStore.getState> =
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<State>(() => ({ 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<State>(() => ({
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<State & Actions>((set) => ({
count: 0,
updateCount: (countCallback) =>
set((state) => ({ count: countCallback(state.count) })),
}))
```
**Jotai**
```ts
import { atom } from 'jotai'
const countAtom = atom<number>(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<State & Actions>((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<number>(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<State & Actions>((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<State & Actions>((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
---
<div class="flex justify-center mb-4">
<img src="../../bear.jpg" alt="Logo Zustand" />
</div>
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 <h1>{bears} bears around here...</h1>
}
function Controls() {
const increasePopulation = useBear((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
```
================================================
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<T>()(...)` (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<BearState>()((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}))
```
<details>
<summary>Why can't we simply infer the type from the initial state?</summary>
<br/>
**TLDR**: Because state generic `T` is invariant.
Consider this minimal version `create`:
```ts
declare const create: <T>(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 <T>(f: (t: T) => T) => T
const x = createFoo((_) => 'hello')
```
Here again, `x` is `unknown` instead of `string`.
<details>
<summary>More about the inference (just for the people curious and interested in TypeScript)</summary>
In some sense this inference failure is not a problem because a value of type `<T>(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.
</details>
</details>
<details>
<summary>Why the currying `()(...)`?</summary>
<br/>
**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: <T, E>(
p: Promise<T>,
) => Promise<[error: undefined, value: T] | [error: E, value: undefined]>
declare const doSomething: () => Promise<string>
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: {
<E>(): <T>(
p: Promise<T>,
) => Promise<[error: undefined, value: T] | [error: E, value: undefined]>
<T, E>(
p: Promise<T>,
): Promise<[error: undefined, value: T] | [error: E, value: undefined]>
}
declare const doSomething: () => Promise<string>
interface Foo {
bar: string
}
const main = async () => {
let [error, value] = await withError<Foo>()(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.
</details>
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 })),
})),
)
```
<details>
<summary>Be a little careful</summary>
<br/>
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.
</details>
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<typeof useBearStore>
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<BearState>()(
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<BearState>()(
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<BearState>()((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<T, Mps, Mcs>,
name?: string,
) => StateCreator<T, Mps, Mcs>
type LoggerImpl = <T>(
f: StateCreator<T, [], []>,
name?: string,
) => StateCreator<T, [], []>
const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => {
const loggedSet: typeof set = (...a) => {
set(...(a as Parameters<typeof set>))
console.log(...(name ? [`${name}:`] : []), get())
}
const setState = store.setState
store.setState = (...a) => {
setState(...(a as Parameters<typeof setState>))
console.log(...(name ? [`${name}:`] : []), store.getState())
}
return f(loggedSet, get, store)
}
export const logger = loggerImpl as unknown as Logger
// ---
const useBearStore = create<BearState>()(
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<T, [...Mps, ['foo', A]], Mcs>,
bar: A,
) => StateCreator<T, Mps, [['foo', A], ...Mcs]>
declare module 'zustand' {
interface StoreMutators<S, A> {
foo: Write<Cast<S, object>, { foo: A }>
}
}
type FooImpl = <T, A>(
f: StateCreator<T, [], []>,
bar: A,
) => StateCreator<T, [], []>
const fooImpl: FooImpl = (f, bar) => (set, get, _store) => {
type T = ReturnType<typeof f>
type A = typeof bar
const store = _store as Mutate<StoreApi<T>, [['foo', A]]>
store.foo = bar
return f(set, get, _store)
}
export const foo = fooImpl as unknown as Foo
type Write<T extends object, U extends object> = Omit<T, keyof U> & U
type Cast<T, U> = 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<T>()(...)`. 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<BearSlice & FishSlice & SharedSlice>()((...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<MyState, [], [], MySlice>` with `StateCreator<MyState, Mutators, [], MySlice>`. For example, if you are using `devtools` then it will be `StateCreator<MyState, [["zustand/devtools", never]], [], MySlice>`. 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<BearState>()((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}))
function useBearStore(): BearState
function useBearStore<T>(selector: (state: BearState) => T): T
function useBearStore<T>(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<BearState>()((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}))
const createBoundedUseStore = ((store) => (selector) =>
useStore(store, selector)) as <S extends StoreApi<unknown>>(
store: S,
) => {
(): ExtractState<S>
<T>(selector: (state: ExtractState<S>) => T): T
}
type ExtractState<S> = S extends { getState: () => infer X } ? X : never
const useBearStore = createBoundedUseStore(bearStore)
```
## Middlewares and their mutators reference
- `devtools` — `["zustand/devtools", never]`
- `persist` — `["zustand/persist", YourPersistedState]`<br/>
`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<YourState>`. 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> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never
const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(
_store: S,
) => {
const store = _store as WithSelectors<typeof _store>
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<BearState>()((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> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never
const createSelectors = <S extends StoreApi<object>>(_store: S) => {
const store = _store as WithSelectors<typeof _store>
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<BearState>()((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 `<BearState>` 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<StoreApi<BearState>>`.
```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<BearState>()((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 <h1>{bears} bears around</h1>
}
```
### 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<BearState>()((set) => ({
...initialState,
increase: (by) => set((s) => ({ bears: s.bears + by })),
reset: () => set(initialState),
}))
function ResetZoo() {
const { bears, increase, reset } = useBearStore()
return (
<div>
<div>{bears}</div>
<button onClick={() => increase(5)}>Increase by 5</button>
<button onClick={reset}>Reset</button>
</div>
)
}
```
### 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<typeof useBearStore>
```
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<BearState>()(() => ({
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 (
<div>
We have {food} units of food for {bears} bears
</div>
)
}
```
#### 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<BearState>()(() => ({
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 <div>We need ${totalFood} jars of honey</div>
}
```
### 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<BearState>()(
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<BearState>()(
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<BearState>()(
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<void>
}
export const useBearStore = create<BearState>()((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<BearState>()((set) => ({
bears: 2,
addBear: () => set((s) => ({ bears: s.bears + 1 })),
}))
// Fish store with explicit types
interface FishState {
fish: number
addFish: () => void
}
const useFishStore = create<FishState>()((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 (
<div>
<div>
{bears} bears and {fish} fish
</div>
<button onClick={addBear}>Add bear</button>
<button onClick={addFish}>Add fish</button>
</div>
)
}
```
### 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<LocalAndUrlStore>(() => persistentStorage),
}
const useLocalAndUrlStore = create()(
persist<LocalAndUrlStore>(
(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<State & Actions>()((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 = (<T>() => {
return (stateCreator: StateCreator<T>) => {
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<typeof createBearStore>
const createBearStore = (initProps?: Partial<BearProps>) => {
const DEFAULT_PROPS: BearProps = {
bears: 0,
}
return createStore<BearState>()((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<BearStore | null>(null)
```
## Basic component usage
```tsx
// Provider implementation
import { useState } from 'react'
function App() {
const [store] = useState(() => createBearStore())
return (
<BearContext.Provider value={store}>
<BasicConsumer />
</BearContext.Provider>
)
}
```
```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 (
<>
<div>{bears} Bears.</div>
<button onClick={addBear}>Add bear</button>
</>
)
}
```
## Common patterns
### Wrapping the context provider
```tsx
// Provider wrapper
import { useState } from 'react'
type BearProviderProps = React.PropsWithChildren<BearProps>
function BearProvider({ children, ...props }: BearProviderProps) {
const [store] = useState(() => createBearStore(props))
return <BearContext.Provider value={store}>{children}</BearContext.Provider>
}
```
### Extracting context logic into a custom hook
```tsx
// Mimic the hook returned by `create`
import { useContext } from 'react'
import { useStore } from 'zustand'
function useBearContext<T>(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 (
<>
<div>{bears} Bears.</div>
<button onClick={addBear}>Add bear</button>
</>
)
}
```
### 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:
<ul>
{bearMealsOrder.map((meal) => (
<li key={meal}>{meal}</li>
))}
</ul>
</>
)
}
```
### 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<T>(
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 (
<BearProvider bears={2}>
<HookConsumer />
</BearProvider>
)
}
```
================================================
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<CounterStore>()((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<typeof createCounterStore>
export const CounterStoreContext = createContext<CounterStoreApi | undefined>(
undefined,
)
export interface CounterStoreProviderProps {
children: ReactNode
}
export const CounterStoreProvider = ({
children,
}: CounterStoreProviderProps) => {
const [store] = useState(() => createCounterStore())
return (
<CounterStoreContext.Provider value={store}>
{children}
</CounterStoreContext.Provider>
)
}
export const useCounterStore = <T,>(
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 (
<div>
Count: {count}
<hr />
<button type="button" onClick={incrementCount}>
Increment Count
</button>
<button type="button" onClick={decrementCount}>
Decrement Count
</button>
</div>
)
}
```
```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 (
<CounterStoreProvider>
<Component {...pageProps} />
</CounterStoreProvider>
)
}
```
```tsx
// src/pages/index.tsx
import { HomePage } from '@/components/pages/home-page'
export default function Home() {
return <HomePage />
}
```
> **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 (
<CounterStoreProvider>
<HomePage />
</CounterStoreProvider>
)
}
```
#### 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 (
<div>
Count: {count}
<hr />
<button type="button" onClick={incrementCount}>
Increment Count
</button>
<button type="button" onClick={decrementCount}>
Decrement Count
</button>
</div>
)
}
```
```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 (
<html lang="en">
<body className={inter.className}>
<CounterStoreProvider>{children}</CounterStoreProvider>
</body>
</html>
)
}
```
```tsx
// src/app/page.tsx
import { HomePage } from '@/components/pages/home-page'
export default function Home() {
return <HomePage />
}
```
> **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 (
<CounterStoreProvider>
<HomePage />
</CounterStoreProvider>
)
}
```
================================================
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 <div>{names.join(', ')}</div>
}
```
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 <div>{names.join(', ')}</div>
}
```
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 (
<div>
<h2>Number of bears: {bears}</h2>
<h2>Number of fishes: {fishes}</h2>
<button onClick={() => addBear()}>Add a bear</button>
</div>
)
}
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 (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Static Server-side-rendered App</title>
</head>
<body>
<div>Hello World!</div>
</body>
</html>
)
}
```
```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(<App />, {
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 (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Static Server-side-rendered App</title>
</head>
<body>
<div>Hello World!</div>
</body>
</html>
)
}
```
```tsx
// main.tsx
import ReactDOMClient from 'react-dom/client'
import { App } from './app.tsx'
ReactDOMClient.hydrateRoot(document, <App />)
```
```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(<App />, {
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<CounterStore> = (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<typeof ZustandExportedTypes>('zustand')
// a variable to hold reset functions for all stores declared in the app
export const storeResetFns = new Set<() => void>()
const createUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
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 = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
console.log('zustand create mock')
// to support curried version of create
return typeof stateCreator === 'function'
? createUncurried(stateCreator)
: createUncurried
}) as typeof ZustandExportedTypes.create
const createStoreUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
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 = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
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<typeof ZustandExportedTypes>('zustand')
// a variable to hold reset functions for all stores declared in the app
export const storeResetFns = new Set<() => void>()
const createUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
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 = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
console.log('zustand create mock')
// to support curried version of create
return typeof stateCreator === 'function'
? createUncurried(stateCreator)
: createUncurried
}) as typeof ZustandExportedTypes.create
const createStoreUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
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 = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
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
/// <reference types="vite/client" />
/// <reference types="vitest/globals" />
```
> **Note**: without [globals configuration](https://vitest.dev/config/#globals) enabled, we do
> need to remove `/// <reference types="vitest/globals" />`.
```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<CounterStore> = (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<CounterStore>()(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<CounterStore>(counterStoreCreator)
}
export type CounterStoreApi = ReturnType<typeof createCounterStore>
export const CounterStoreContext = createContext<CounterStoreApi | undefined>(
undefined,
)
export interface CounterStoreProviderProps {
children: ReactNode
}
export const CounterStoreProvider = ({
children,
}: CounterStoreProviderProps) => {
const [store] = useState(() => createCounterStore())
return (
<CounterStoreContext.Provider value={store}>
{children}
</CounterStoreContext.Provider>
)
}
export type UseCounterStoreContextSelector<T> = (store: CounterStore) => T
export const useCounterStoreContext = <T,>(
selector: UseCounterStoreContextSelector<T>,
): 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 (
<div>
<h2>Counter Store</h2>
<h4>{count}</h4>
<button onClick={inc}>One Up</button>
</div>
)
}
```
```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(<Counter />)
}
```
```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 (
<div>
<h2>Counter Store Context</h2>
<h4>{count}</h4>
<button onClick={inc}>One Up</button>
</div>
)
}
export const CounterWithContext = () => {
return (
<CounterStoreProvider>
<Counter />
</CounterStoreProvider>
)
}
```
```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(<CounterWithContext />)
}
```
> **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<CounterStore> = (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<CounterStore>()(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<CounterStore>(counterStoreCreator)
}
export type CounterStoreApi = ReturnType<typeof createCounterStore>
export const CounterStoreContext = createContext<CounterStoreApi | undefined>(
undefined,
)
export interface CounterStoreProviderProps {
children: ReactNode
}
export const CounterStoreProvider = ({
children,
}: CounterStoreProviderProps) => {
const [store] = useState(() => createCounterStore())
return (
<CounterStoreContext.Provider value={store}>
{children}
</CounterStoreContext.Provider>
)
}
export type UseCounterStoreContextSelector<T> = (store: CounterStore) => T
export const useCounterStoreContext = <T,>(
selector: UseCounterStoreContextSelector<T>,
): 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 (
<div>
<h2>Counter Store</h2>
<h4>{count}</h4>
<button onClick={inc}>One Up</button>
</div>
)
}
```
```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(<Counter />)
}
```
```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 (
<div>
<h2>Counter Store Context</h2>
<h4>{count}</h4>
<button onClick={inc}>One Up</button>
</div>
)
}
export const CounterWithContext = () => {
return (
<CounterStoreProvider>
<Counter />
</CounterStoreProvider>
)
}
```
```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(<CounterWithContext />, {
wrapper: ({ children }) => (
<CounterStoreContext.Provider value={store}>
{children}
</CounterStoreContext.Provider>
),
})
}
```
## 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 (
<button
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
backgroundColor: '#fff',
border: '1px solid #999',
outline: 0,
borderRadius: 0,
fontSize: '1rem',
fontWeight: 'bold',
}}
onClick={onSquareClick}
>
{value}
</button>
)
}
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 (
<>
<div style={{ marginBottom: '0.5rem' }}>{status}</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
width: 'calc(3 * 2.5rem)',
height: 'calc(3 * 2.5rem)',
border: '1px solid #999',
}}
>
{squares.map((_, i) => (
<Square
key={`square-${i}`}
value={squares[i]}
onSquareClick={() => handleClick(i)}
/>
))}
</div>
</>
)
}
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 (
<div
style={{
display: 'flex',
flexDirection: 'row',
fontFamily: 'monospace',
}}
>
<div>
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div style={{ marginLeft: '1rem' }}>
<ol>
{history.map((_, historyIndex) => {
const description =
historyIndex > 0
? `Go to move #${historyIndex}`
: 'Go to game start'
return (
<li key={historyIndex}>
<button onClick={() => jumpTo(historyIndex)}>
{description}
</button>
</li>
)
})}
</ol>
</div>
</div>
)
}
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
`<button>` element, styled to look like a square. The button displays the value prop, which can be
`'X'`, `'O'`, or `null`, depending on the game's state. When the button is clicked, it triggers the
`onSquareClick` function passed in as a prop, allowing the game to respond to user input.
Here's the code for the `Square` component:
```jsx
function Square({ value, onSquareClick }) {
return (
<button
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
backgroundColor: '#fff',
border: '1px solid #999',
outline: 0,
borderRadius: 0,
fontSize: '1rem',
fontWeight: 'bold',
}}
onClick={onSquareClick}
>
{value}
</button>
)
}
```
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 `<div>` 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 (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
width: 'calc(3 * 2.5rem)',
height: 'calc(3 * 2.5rem)',
border: '1px solid #999',
}}
>
<Square value="1" />
<Square value="2" />
<Square value="3" />
<Square value="4" />
<Square value="5" />
<Square value="6" />
<Square value="7" />
<Square value="8" />
<Square value="9" />
</div>
)
}
```
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 (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
width: 'calc(3 * 2.5rem)',
height: 'calc(3 * 2.5rem)',
border: '1px solid #999',
}}
>
{squares.map((square, squareIndex) => (
<Square key={squareIndex} value={square} />
))}
</div>
)
}
```
`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
<Square key={squareIndex} value={square} onSquareClick={() => 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 (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
width: 'calc(3 * 2.5rem)',
height: 'calc(3 * 2.5rem)',
border: '1px solid #999',
}}
>
{squares.map((square, squareIndex) => (
<Square
key={squareIndex}
value={square}
onSquareClick={() => handleClick(squareIndex)}
/>
))}
</div>
)
}
```
> [!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 (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
width: 'calc(3 * 2.5rem)',
height: 'calc(3 * 2.5rem)',
border: '1px solid #999',
}}
>
{squares.map((square, squareIndex) => (
<Square
key={squareIndex}
value={square}
onSquareClick={() => handleClick(squareIndex)}
/>
))}
</div>
)
}
```
### 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 (
<>
<div style={{ marginBottom: '0.5rem' }}>{status}</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
width: 'calc(3 * 2.5rem)',
height: 'calc(3 * 2.5rem)',
border: '1px solid #999',
}}
>
{squares.map((square, squareIndex) => (
<Square
key={squareIndex}
value={square}
onSquareClick={() => handleClick(squareIndex)}
/>
))}
</div>
</>
)
}
```
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 (
<button
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
backgroundColor: '#fff',
border: '1px solid #999',
outline: 0,
borderRadius: 0,
fontSize: '1rem',
fontWeight: 'bold',
}}
onClick={onSquareClick}
>
{value}
</button>
)
}
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 (
<>
<div style={{ marginBottom: '0.5rem' }}>{status}</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
width: 'calc(3 * 2.5rem)',
height: 'calc(3 * 2.5rem)',
border: '1px solid #999',
}}
>
{squares.map((square, squareIndex) => (
<Square
key={squareIndex}
value={square}
onSquareClick={() => handleClick(squareIndex)}
/>
))}
</div>
</>
)
}
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 (
<>
<div style={{ marginBottom: '0.5rem' }}>{status}</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
width: 'calc(3 * 2.5rem)',
height: 'calc(3 * 2.5rem)',
border: '1px solid #999',
}}
>
{squares.map((square, squareIndex) => (
<Square
key={squareIndex}
value={square}
onSquareClick={() => handleClick(squareIndex)}
/>
))}
</div>
</>
)
}
export default function Game() {
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
fontFamily: 'monospace',
}}
>
<div>
<Board />
</div>
<div style={{ marginLeft: '1rem' }}>
<ol>{/* TODO */}</ol>
</div>
</div>
)
}
```
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 (
<div
style={{
display: 'flex',
flexDirection: 'row',
fontFamily: 'monospace',
}}
>
<div>
<Board />
</div>
<div style={{ marginLeft: '1rem' }}>
<ol>{/*TODO*/}</ol>
</div>
</div>
)
}
```
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 (
<div
style={{
display: 'flex',
flexDirection: 'row',
fontFamily: 'monospace',
}}
>
<div>
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div style={{ marginLeft: '1rem' }}>
<ol>{/*TODO*/}</ol>
</div>
</div>
)
}
```
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 (
<>
<div style={{ marginBottom: '0.5rem' }}>{status}</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
width: 'calc(3 * 2.5rem)',
height: 'calc(3 * 2.5rem)',
border: '1px solid #999',
}}
>
{squares.map((square, squareIndex) => (
<Square
key={squareIndex}
value={square}
onSquareClick={() => handleClick(squareIndex)}
/>
))}
</div>
</>
)
}
```
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 (
<button
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
backgroundColor: '#fff',
border: '1px solid #999',
outline: 0,
borderRadius: 0,
fontSize: '1rem',
fontWeight: 'bold',
}}
onClick={onSquareClick}
>
{value}
</button>
)
}
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 (
<>
<div style={{ marginBottom: '0.5rem' }}>{status}</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
width: 'calc(3 * 2.5rem)',
height: 'calc(3 * 2.5rem)',
border: '1px solid #999',
}}
>
{squares.map((square, squareIndex) => (
<Square
key={squareIndex}
value={square}
onSquareClick={() => handleClick(squareIndex)}
/>
))}
</div>
</>
)
}
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 (
<div
style={{
display: 'flex',
flexDirection: 'row',
fontFamily: 'monospace',
}}
>
<div>
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div style={{ marginLeft: '1rem' }}>
<ol>{/*TODO*/}</ol>
</div>
</div>
)
}
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 (
<div
style={{
display: 'flex',
flexDirection: 'row',
fontFamily: 'monospace',
}}
>
<div>
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div style={{ marginLeft: '1rem' }}>
<ol>
{history.map((_, historyIndex) => {
const description =
historyIndex > 0
? `Go to move #${historyIndex}`
: 'Go to game start'
return (
<li key={historyIndex}>
<button onClick={() => jumpTo(historyIndex)}>
{description}
</button>
</li>
)
})}
</ol>
</div>
</div>
)
}
```
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 (
<div
style={{
display: 'flex',
flexDirection: 'row',
fontFamily: 'monospace',
}}
>
<div>
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div style={{ marginLeft: '1rem' }}>
<ol>
{history.map((_, historyIndex) => {
const description =
historyIndex > 0
? `Go to move #${historyIndex}`
: 'Go to game start'
return (
<li key={historyIndex}>
<button onClick={() => jumpTo(historyIndex)}>
{description}
</button>
</li>
)
})}
</ol>
</div>
</div>
)
}
```
### 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 (
<div
style={{
display: 'flex',
flexDirection: 'row',
fontFamily: 'monospace',
}}
>
<div>
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div style={{ marginLeft: '1rem' }}>
<ol>
{history.map((_, historyIndex) => {
const description =
historyIndex > 0
? `Go to move #${historyIndex}`
: 'Go to game start'
return (
<li key={historyIndex}>
<button onClick={() => jumpTo(historyIndex)}>
{description}
</button>
</li>
)
})}
</ol>
</div>
</div>
)
}
```
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<State & Action>((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 (
<main>
<label>
First name
<input
// Update the "firstName" state
onChange={(e) => updateFirstName(e.currentTarget.value)}
value={firstName}
/>
</label>
<p>
Hello, <strong>{firstName}!</strong>
</p>
</main>
)
}
```
## 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<State>().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<T>()(stateCreatorFn: StateCreator<T, [], []>): StoreApi<T>
```
## 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<AgeStore>()((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<typeof ageStore.subscribe>[0] = (state) => {
$yourAgeHeading.innerHTML = `Your age: ${state.age}`
}
render(ageStore.getInitialState(), ageStore.getInitialState())
ageStore.subscribe(render)
```
Here's the `html` code
```html
<h1 id="your-age"></h1>
<button id="increment-by-3" type="button">+3</button>
<button id="increment-by-1" type="button">+1</button>
```
### 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<XStore>()(() => 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<typeof xStore.subscribe>[0] = (x) => {
$dot.style.transform = `translate(${x}px, 0)`
}
render(xStore.getInitialState(), xStore.getInitialState())
xStore.subscribe(render)
```
Here's the `html` code
```html
<div
id="dot-container"
style="position: relative; width: 100vw; height: 100vh;"
>
<div
id="dot"
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
></div>
</div>
```
### 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<PositionStore>()((set) => ({
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
SYMBOL INDEX (216 symbols across 31 files)
FILE: examples/demo/src/App.jsx
function Counter (line 11) | function Counter() {
function App (line 21) | function App() {
FILE: examples/demo/src/components/CodePreview.jsx
function CodePreview (line 15) | function CodePreview() {
FILE: examples/demo/src/components/CopyButton.jsx
function CopyButton (line 8) | function CopyButton({ code, ...props }) {
FILE: examples/demo/src/components/Details.jsx
function Details (line 1) | function Details() {
FILE: examples/demo/src/components/Fireflies.jsx
function Fatline (line 10) | function Fatline({ curve, color }) {
function Fireflies (line 31) | function Fireflies({ count, colors, radius = 10 }) {
FILE: examples/demo/src/components/Scene.jsx
function Experience (line 20) | function Experience() {
function Effects (line 130) | function Effects() {
function FallbackScene (line 150) | function FallbackScene() {
function Scene (line 178) | function Scene() {
function Canvas (line 193) | function Canvas({ children, onError }) {
FILE: examples/demo/src/components/SnippetLang.jsx
function SnippetLang (line 1) | function SnippetLang({ lang, setLang }) {
FILE: examples/starter/src/index.tsx
type Store (line 9) | type Store = {
function App (line 36) | function App() {
FILE: rollup.config.mjs
function external (line 18) | function external(id) {
function getEsbuild (line 22) | function getEsbuild() {
function createDeclarationConfig (line 30) | function createDeclarationConfig(input, output) {
function createESMConfig (line 47) | function createESMConfig(input, output) {
function createCommonJSConfig (line 75) | function createCommonJSConfig(input, output) {
FILE: src/middleware/combine.ts
type Write (line 3) | type Write<T, U> = Omit<T, keyof U> & U
function combine (line 5) | function combine<
FILE: src/middleware/devtools.ts
type Config (line 9) | type Config = Parameters<
type StoreMutators (line 17) | interface StoreMutators<S, A> {
type Message (line 23) | type Message = {
type WithDispatch (line 29) | type WithDispatch = {
type Cast (line 38) | type Cast<T, U> = T extends U ? T : U
type Write (line 39) | type Write<T, U> = Omit<T, keyof U> & U
type TakeTwo (line 40) | type TakeTwo<T> = T extends { length: 0 }
type WithDevtools (line 60) | type WithDevtools<S> = Write<S, StoreDevtools<S>>
type Action (line 62) | type Action =
type StoreDevtools (line 68) | type StoreDevtools<S> = S extends {
type DevtoolsOptions (line 84) | interface DevtoolsOptions extends Config {
type Devtools (line 91) | type Devtools = <
type StoreMutators (line 103) | interface StoreMutators<S, A> {
type DevtoolsImpl (line 108) | type DevtoolsImpl = <T>(
type NamedSet (line 113) | type NamedSet<T> = WithDevtools<StoreApi<T>>['setState']
type Connection (line 115) | type Connection = ReturnType<
type ConnectionName (line 118) | type ConnectionName = string | undefined
type StoreName (line 119) | type StoreName = string
type StoreInformation (line 120) | type StoreInformation = StoreApi<unknown>
type ConnectionInformation (line 121) | type ConnectionInformation = {
type S (line 192) | type S = ReturnType<typeof fn> & {
type PartialState (line 195) | type PartialState = Partial<S> | ((s: S) => Partial<S>)
FILE: src/middleware/immer.ts
type Immer (line 5) | type Immer = <
type StoreMutators (line 16) | interface StoreMutators<S, A> {
type Write (line 21) | type Write<T, U> = Omit<T, keyof U> & U
type SkipTwo (line 22) | type SkipTwo<T> = T extends { length: 0 }
type SetStateType (line 36) | type SetStateType<T extends unknown[]> = Exclude<T[0], (...args: any[]) ...
type WithImmer (line 38) | type WithImmer<S> = Write<S, StoreImmer<S>>
type StoreImmer (line 40) | type StoreImmer<S> = S extends {
type ImmerImpl (line 70) | type ImmerImpl = <T>(
type T (line 75) | type T = ReturnType<typeof initializer>
FILE: src/middleware/persist.ts
type StateStorage (line 7) | interface StateStorage<R = unknown> {
type StorageValue (line 13) | type StorageValue<S> = {
type PersistStorage (line 18) | interface PersistStorage<S, R = unknown> {
type JsonStorageOptions (line 26) | type JsonStorageOptions = {
function createJSONStorage (line 31) | function createJSONStorage<S, R = unknown>(
type PersistOptions (line 63) | interface PersistOptions<
type PersistListener (line 123) | type PersistListener<S> = (state: S) => void
type StorePersist (line 125) | type StorePersist<S, Ps, Pr> = S extends {
type Thenable (line 148) | type Thenable<Value> = {
method then (line 168) | then(onFulfilled) {
method catch (line 171) | catch(_onRejected) {
method then (line 177) | then(_onFulfilled) {
method catch (line 180) | catch(onRejected) {
type S (line 188) | type S = ReturnType<typeof config>
type Persist (line 377) | type Persist = <
type StoreMutators (line 388) | interface StoreMutators<S, A> {
type Write (line 393) | type Write<T, U> = Omit<T, keyof U> & U
type WithPersist (line 395) | type WithPersist<S, A> = Write<S, StorePersist<S, A, unknown>>
type PersistImpl (line 397) | type PersistImpl = <T>(
FILE: src/middleware/redux.ts
type Write (line 4) | type Write<T, U> = Omit<T, keyof U> & U
type Action (line 6) | type Action = { type: string }
type StoreRedux (line 8) | type StoreRedux<A> = {
type ReduxState (line 13) | type ReduxState<A> = {
type WithRedux (line 17) | type WithRedux<S, A> = Write<S, StoreRedux<A>>
type Redux (line 19) | type Redux = <
type StoreMutators (line 29) | interface StoreMutators<S, A> {
type ReduxImpl (line 34) | type ReduxImpl = <T, A extends Action>(
type S (line 40) | type S = typeof initial
type A (line 41) | type A = Parameters<typeof reducer>[1]
FILE: src/middleware/ssrSafe.ts
function ssrSafe (line 6) | function ssrSafe<
FILE: src/middleware/subscribeWithSelector.ts
type SubscribeWithSelector (line 3) | type SubscribeWithSelector = <
type Write (line 15) | type Write<T, U> = Omit<T, keyof U> & U
type WithSelectorSubscribe (line 17) | type WithSelectorSubscribe<S> = S extends { getState: () => infer T }
type StoreMutators (line 23) | interface StoreMutators<S, A> {
type StoreSubscribeWithSelector (line 28) | type StoreSubscribeWithSelector<T> = {
type SubscribeWithSelectorImpl (line 42) | type SubscribeWithSelectorImpl = <T extends object>(
type S (line 48) | type S = ReturnType<typeof fn>
type Listener (line 49) | type Listener = (state: S, previousState: S) => void
FILE: src/react.ts
type ReadonlyStoreApi (line 11) | type ReadonlyStoreApi<T> = Pick<
function useStore (line 26) | function useStore<TState, StateSlice>(
type UseBoundStore (line 39) | type UseBoundStore<S extends ReadonlyStoreApi<unknown>> = {
type Create (line 44) | type Create = {
FILE: src/react/shallow.ts
function useShallow (line 4) | function useShallow<S, U>(selector: (state: S) => U): (state: S) => U {
FILE: src/traditional.ts
type ReadonlyStoreApi (line 14) | type ReadonlyStoreApi<T> = Pick<
function useStoreWithEqualityFn (line 31) | function useStoreWithEqualityFn<TState, StateSlice>(
type UseBoundStoreWithEqualityFn (line 47) | type UseBoundStoreWithEqualityFn<S extends ReadonlyStoreApi<unknown>> = {
type CreateWithEqualityFn (line 55) | type CreateWithEqualityFn = {
FILE: src/types.d.ts
type ImportMeta (line 1) | interface ImportMeta {
FILE: src/vanilla.ts
type SetStateInternal (line 1) | type SetStateInternal<T> = {
type StoreApi (line 9) | interface StoreApi<T> {
type ExtractState (line 16) | type ExtractState<S> = S extends { getState: () => infer T } ? T : never
type Get (line 18) | type Get<T, K, F> = K extends keyof T ? T[K] : F
type Mutate (line 20) | type Mutate<S, Ms> = number extends Ms['length' & keyof Ms]
type StateCreator (line 28) | type StateCreator<
type StoreMutators (line 40) | interface StoreMutators<S, A> {}
type StoreMutatorIdentifier (line 41) | type StoreMutatorIdentifier = keyof StoreMutators<unknown, unknown>
type CreateStore (line 43) | type CreateStore = {
type CreateStoreImpl (line 53) | type CreateStoreImpl = <
type TState (line 61) | type TState = ReturnType<typeof createState>
type Listener (line 62) | type Listener = (state: TState, prevState: TState) => void
FILE: src/vanilla/shallow.ts
function shallow (line 48) | function shallow<T>(valueA: T, valueB: T): boolean {
FILE: tests/basic.test.tsx
type CounterState (line 45) | type CounterState = {
function Counter (line 56) | function Counter() {
function Counter (line 77) | function Counter() {
function Component (line 101) | function Component() {
function Counter (line 139) | function Counter() {
function Control (line 145) | function Control() {
function Counter (line 172) | function Counter() {
type State (line 193) | type State = { one: string; two: string }
type Props (line 194) | type Props = { selector: (state: State) => string }
function Component (line 200) | function Component({ selector }: Props) {
type State (line 220) | type State = { value: number }
type Props (line 221) | type Props = { equalityFn: (a: State, b: State) => boolean }
function Component (line 230) | function Component({ equalityFn }: Props) {
type State (line 263) | type State = { value: number }
type Props (line 264) | type Props = {
function Component (line 276) | function Component({ selector, equalityFn }: Props) {
type State (line 323) | type State = { value: string | number }
class ErrorBoundary (line 332) | class ErrorBoundary extends ClassComponent<
method constructor (line 336) | constructor(props: { children?: ReactNode | undefined }) {
method getDerivedStateFromError (line 340) | static getDerivedStateFromError() {
method render (line 343) | render() {
method constructor (line 386) | constructor(props: { children?: ReactNode | undefined }) {
method getDerivedStateFromError (line 390) | static getDerivedStateFromError() {
method render (line 393) | render() {
function Component (line 349) | function Component() {
type State (line 372) | type State = { value: string | number }
class ErrorBoundary (line 382) | class ErrorBoundary extends ClassComponent<
method constructor (line 336) | constructor(props: { children?: ReactNode | undefined }) {
method getDerivedStateFromError (line 340) | static getDerivedStateFromError() {
method render (line 343) | render() {
method constructor (line 386) | constructor(props: { children?: ReactNode | undefined }) {
method getDerivedStateFromError (line 390) | static getDerivedStateFromError() {
method render (line 393) | render() {
function Component (line 399) | function Component() {
type State (line 421) | type State = {
type State (line 437) | type State = {
type State (line 483) | type State = { a: number; b: number }
function staticSelector (line 488) | function staticSelector(s: State) {
function Component (line 493) | function Component() {
type State (line 521) | type State = { a: number; b: number }
function staticSelector (line 527) | function staticSelector(s: State) {
function Component (line 532) | function Component() {
type State (line 565) | type State = {
type Props (line 568) | type Props = { id: string }
function changeState (line 577) | function changeState() {
function Child (line 585) | function Child({ id }: Props) {
function Parent (line 590) | function Parent() {
function increment (line 618) | function increment() {
function Count (line 622) | function Count() {
function CountWithInitialIncrement (line 627) | function CountWithInitialIncrement() {
function Component (line 632) | function Component() {
function Count1 (line 664) | function Count1() {
function Count2 (line 669) | function Count2() {
FILE: tests/devtools.test.tsx
type TupleOfEqualLengthH (line 7) | type TupleOfEqualLengthH<
type TupleOfEqualLength (line 14) | type TupleOfEqualLength<Arr extends unknown[], T> = number extends Arr['...
type Connection (line 18) | type Connection = {
function assertAllAreDefined (line 32) | function assertAllAreDefined<T>(arr: (T | undefined)[]): asserts arr is ...
function getNamedConnectionApis (line 37) | function getNamedConnectionApis<Keys extends (string | undefined)[]>(
function getNamedConnectionSubscribers (line 44) | function getNamedConnectionSubscribers<Keys extends (string | undefined)...
function getUnnamedConnectionApis (line 58) | function getUnnamedConnectionApis<Keys extends string[]>(...keys: Keys) {
function getUnnamedConnectionSubscribers (line 63) | function getUnnamedConnectionSubscribers<Keys extends string[]>(...keys:...
function getKeyFromOptions (line 78) | function getKeyFromOptions(options: any): string | undefined {
FILE: tests/middlewareTypes.test.tsx
type CounterState (line 14) | type CounterState = {
type ExampleStateCreator (line 19) | type ExampleStateCreator<T, A> = <
type Write (line 27) | type Write<T, U> = Omit<T, keyof U> & U
type StoreModifyAllButSetState (line 28) | type StoreModifyAllButSetState<S, A> = S extends {
type StoreMutators (line 35) | interface StoreMutators<S, A> {
type OmitFn (line 96) | type OmitFn<T> = Exclude<T, (...args: any[]) => any>
type TableStore (line 184) | type TableStore = {
type MyState (line 682) | type MyState = {
type MyState (line 707) | type MyState = {
type BearSlice (line 791) | interface BearSlice {
type FishSlice (line 797) | interface FishSlice {
FILE: tests/persistAsync.test.tsx
function Counter (line 82) | function Counter() {
function Counter (line 128) | function Counter() {
function Counter (line 165) | function Counter() {
function Counter2 (line 196) | function Counter2() {
function Counter (line 245) | function Counter() {
function Component (line 302) | function Component() {
function Counter (line 357) | function Counter() {
function Counter (line 402) | function Counter() {
function Counter (line 454) | function Counter() {
function Counter (line 498) | function Counter() {
function Counter (line 551) | function Counter() {
function Counter (line 593) | function Counter() {
function Counter (line 743) | function Counter() {
function Counter (line 789) | function Counter() {
function MapDisplay (line 841) | function MapDisplay() {
function MapDisplay (line 879) | function MapDisplay() {
function MapDisplay2 (line 915) | function MapDisplay2() {
FILE: tests/ssr.test.tsx
type BearStoreState (line 7) | interface BearStoreState {
type BearStoreAction (line 11) | interface BearStoreAction {
function Counter (line 21) | function Counter() {
FILE: tests/test-utils.ts
type ReplacedMap (line 1) | type ReplacedMap = {
function sleep (line 35) | function sleep(ms: number): Promise<void> {
FILE: tests/types.test.tsx
type ExampleState (line 12) | type ExampleState = {
function checkAllTypes (line 71) | function checkAllTypes(
type AssertEqual (line 104) | type AssertEqual<Type, Expected> = Type extends Expected
type Count (line 111) | type Count = { count: number }
type State (line 140) | type State = {
type State (line 201) | interface State {
type State (line 221) | interface State {
type State (line 241) | interface State {
FILE: tests/vanilla/basic.test.ts
type CounterState (line 41) | type CounterState = {
type State (line 57) | type State = {
type State (line 81) | type State = {
FILE: tests/vanilla/shallow.test.tsx
function firstFnCompare (line 130) | function firstFnCompare() {
function secondFnCompare (line 134) | function secondFnCompare() {
Condensed preview — 128 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (731K chars).
[
{
"path": ".codesandbox/ci.json",
"chars": 262,
"preview": "{\n \"packages\": [\"dist\"],\n \"sandboxes\": [\n \"new\",\n \"react-typescript-react-ts\",\n \"simple-react-browserify-x9yn"
},
{
"path": ".github/DISCUSSION_TEMPLATE/bug-report.yml",
"chars": 588,
"preview": "labels: ['bug']\nbody:\n - type: markdown\n attributes:\n value: If you don't have a reproduction link, please choo"
},
{
"path": ".github/FUNDING.yml",
"chars": 914,
"preview": "# These are supported funding model platforms\n\ngithub: [dai-shi] # Replace with up to 4 GitHub Sponsors-enabled username"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 171,
"preview": "---\nname: Assigned issue\nabout: This is to create a new issue that already has an assignee. Please open a new discussion"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 469,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: Bug Reports\n url: https://github.com/pmndrs/zustand/discussions/"
},
{
"path": ".github/dependabot.yml",
"chars": 499,
"preview": "version: 2\nupdates:\n - package-ecosystem: 'npm'\n directory: '/'\n schedule:\n interval: 'daily'\n ignore:\n "
},
{
"path": ".github/pull_request_template.md",
"chars": 137,
"preview": "## Related Bug Reports or Discussions\n\nFixes #\n\n## Summary\n\n## Check List\n\n- [ ] `pnpm run fix` for formatting and linti"
},
{
"path": ".github/workflows/compressed-size.yml",
"chars": 570,
"preview": "name: Compressed Size\n\non: [pull_request]\n\njobs:\n compressed_size:\n runs-on: ubuntu-latest\n steps:\n - uses: "
},
{
"path": ".github/workflows/docs.yml",
"chars": 1145,
"preview": "name: Build documentation and deploy to GitHub Pages\n\non:\n push:\n branches: [main]\n workflow_dispatch:\n\n# Cancel pr"
},
{
"path": ".github/workflows/preview-release.yml",
"chars": 568,
"preview": "name: Preview Release\n\non: [push, pull_request]\n\njobs:\n preview_release:\n runs-on: ubuntu-latest\n steps:\n - "
},
{
"path": ".github/workflows/publish.yml",
"chars": 638,
"preview": "name: Publish\n\non:\n release:\n types: [published]\n\npermissions:\n id-token: write\n contents: read\n\njobs:\n publish:\n"
},
{
"path": ".github/workflows/test-multiple-builds.yml",
"chars": 1789,
"preview": "name: Test Multiple Builds\n\non:\n push:\n branches: [main]\n pull_request:\n types: [opened, synchronize]\n\njobs:\n t"
},
{
"path": ".github/workflows/test-multiple-versions.yml",
"chars": 1006,
"preview": "name: Test Multiple Versions\n\non:\n push:\n branches: [main]\n pull_request:\n types: [opened, synchronize]\n\njobs:\n "
},
{
"path": ".github/workflows/test-old-typescript.yml",
"chars": 2627,
"preview": "name: Test Old TypeScript\n\non:\n push:\n branches: [main]\n pull_request:\n types: [opened, synchronize]\n\njobs:\n te"
},
{
"path": ".github/workflows/test.yml",
"chars": 698,
"preview": "name: Test\n\non:\n push:\n branches: [main]\n pull_request:\n types: [opened, synchronize]\n\njobs:\n test:\n runs-on"
},
{
"path": ".gitignore",
"chars": 231,
"preview": "node_modules/\ndist/\nThumbs.db\nehthumbs.db\nDesktop.ini\n$RECYCLE.BIN/\n.DS_Store\n.vscode\n.docz/\ncoverage/\n.rpt2_cache/\n.ide"
},
{
"path": ".prettierignore",
"chars": 20,
"preview": "dist\npnpm-lock.yaml\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 4424,
"preview": "# Contributing\n\n## General Guideline\n\n### Reporting Issues\n\nIf you have found what you think is a bug, please [start a d"
},
{
"path": "FUNDING.json",
"chars": 107,
"preview": "{\n \"drips\": {\n \"ethereum\": {\n \"ownedBy\": \"0xBA918e34bed77Ba7a9fCF53be0A81FA538d56FA7\"\n }\n }\n}\n"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2019 Paul Henschel\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "README.md",
"chars": 17787,
"preview": "<p align=\"center\">\n <img src=\"./docs/bear.jpg\" />\n</p>\n\n[\n\nServer-side Rendering (SSR) is a technique tha"
},
{
"path": "docs/learn/guides/testing.md",
"chars": 21236,
"preview": "---\ntitle: Testing\ndescription: Writing Tests\nnav: 18\n---\n\n## Setting Up a Test Environment\n\n### Test Runners\n\nUsually, "
},
{
"path": "docs/learn/guides/tutorial-tic-tac-toe.md",
"chars": 41893,
"preview": "---\ntitle: 'Tutorial: Tic-Tac-Toe'\ndescription: Building a game\nnav: 3\n---\n\n# Tutorial: Tic-Tac-Toe\n\n## Building a game\n"
},
{
"path": "docs/learn/guides/updating-state.md",
"chars": 3344,
"preview": "---\ntitle: Updating state\nnav: 4\n---\n\n## Flat updates\n\nUpdating state with Zustand is simple! Call the provided `set` fu"
},
{
"path": "docs/learn/index.md",
"chars": 3162,
"preview": "---\ntitle: Learn\ndescription: A guided path to understand Zustand fundamentals, common patterns, and when to reach for s"
},
{
"path": "docs/reference/apis/create-store.md",
"chars": 15027,
"preview": "---\ntitle: createStore\ndescription: How to create vanilla stores\nnav: 22\n---\n\n`createStore` lets you create a vanilla st"
},
{
"path": "docs/reference/apis/create-with-equality-fn.md",
"chars": 17101,
"preview": "---\ntitle: createWithEqualityFn\ndescription: How to create efficient stores\ntag: react\nnav: 23\n---\n\n`createWithEqualityF"
},
{
"path": "docs/reference/apis/create.md",
"chars": 15423,
"preview": "---\ntitle: create\ndescription: How to create stores\ntag: react\nnav: 21\n---\n\n`create` lets you create a React Hook with A"
},
{
"path": "docs/reference/apis/shallow.md",
"chars": 7035,
"preview": "---\ntitle: shallow\ndescription: How compare simple data effectively\nnav: 24\n---\n\n`shallow` lets you run fast checks on s"
},
{
"path": "docs/reference/hooks/use-shallow.md",
"chars": 5967,
"preview": "---\ntitle: useShallow\ndescription: How to memoize selector functions\ntag: react\nnav: 27\n---\n\n`useShallow` is a React Hoo"
},
{
"path": "docs/reference/hooks/use-store-with-equality-fn.md",
"chars": 23751,
"preview": "---\ntitle: useStoreWithEqualityFn\ndescription: How to use vanilla stores effectively in React\ntag: react\nnav: 26\n---\n\n`u"
},
{
"path": "docs/reference/hooks/use-store.md",
"chars": 22451,
"preview": "---\ntitle: useStore\ndescription: How to use vanilla stores in React\ntag: react\nnav: 25\n---\n\n`useStore` is a React Hook t"
},
{
"path": "docs/reference/index.md",
"chars": 2628,
"preview": "---\ntitle: Reference\ndescription: API-first reference for stores, hooks, middlewares, and integrations.\n---\n\n## APIs\n\nCo"
},
{
"path": "docs/reference/integrations/immer-middleware.md",
"chars": 3099,
"preview": "---\ntitle: Immer middleware\nnav: 35\n---\n\nThe [Immer](https://github.com/immerjs/immer) middleware enables you\nto use imm"
},
{
"path": "docs/reference/integrations/persisting-store-data.md",
"chars": 20311,
"preview": "---\ntitle: Persisting store data\nnav: 34\n---\n\nThe Persist middleware enables you to store\nyour Zustand state in a storag"
},
{
"path": "docs/reference/integrations/third-party-libraries.md",
"chars": 9778,
"preview": "---\ntitle: Third-party Libraries\nnav: 36\n---\n\nZustand provides bear necessities for state management.\nAlthough it is gre"
},
{
"path": "docs/reference/middlewares/combine.md",
"chars": 2763,
"preview": "---\ntitle: combine\ndescription: How to create a store and get types automatically inferred\nnav: 32\n---\n\n# combine\n\n`comb"
},
{
"path": "docs/reference/middlewares/devtools.md",
"chars": 8985,
"preview": "---\ntitle: devtools\ndescription: How to time-travel debug your store\nnav: 29\n---\n\n# devtools\n\n`devtools` middleware lets"
},
{
"path": "docs/reference/middlewares/immer.md",
"chars": 5945,
"preview": "---\ntitle: immer\ndescription: How to perform immutable updates in a store without boilerplate code\nnav: 31\n---\n\n# immer\n"
},
{
"path": "docs/reference/middlewares/persist.md",
"chars": 27380,
"preview": "---\ntitle: persist\ndescription: How to persist a store\nnav: 28\n---\n\n# persist\n\n`persist` middleware lets you persist a s"
},
{
"path": "docs/reference/middlewares/redux.md",
"chars": 4011,
"preview": "---\ntitle: redux\ndescription: How to use actions and reducers in a store\nnav: 30\n---\n\n# redux\n\n`redux` middleware lets y"
},
{
"path": "docs/reference/middlewares/subscribe-with-selector.md",
"chars": 2945,
"preview": "---\ntitle: subscribeWithSelector\ndescription: How to subscribe to granular store updates in a store\nnav: 33\n---\n\n# subsc"
},
{
"path": "docs/reference/migrations/migrating-to-v4.md",
"chars": 7814,
"preview": "---\ntitle: Migrating to v4\nnav: 38\n---\n\nThe only breaking changes are in types.\nIf you are using Zustand with TypeScript"
},
{
"path": "docs/reference/migrations/migrating-to-v5.md",
"chars": 5790,
"preview": "---\ntitle: How to Migrate to v5 from v4\nnav: 37\n---\n\n# How to Migrate to v5 from v4\n\nWe highly recommend to update to th"
},
{
"path": "docs/reference/previous-versions/zustand-v3-create-context.md",
"chars": 3162,
"preview": "---\ntitle: createContext from zustand/context\nnav: 39\n---\n\nA special `createContext` is provided since v3.5,\nwhich avoid"
},
{
"path": "eslint.config.mjs",
"chars": 2874,
"preview": "import eslint from '@eslint/js'\nimport vitest from '@vitest/eslint-plugin'\nimport { defineConfig, globalIgnores } from '"
},
{
"path": "examples/demo/.gitignore",
"chars": 253,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": "examples/demo/eslint.config.js",
"chars": 996,
"preview": "import eslint from '@eslint/js'\nimport react from 'eslint-plugin-react'\nimport reactHooks from 'eslint-plugin-react-hook"
},
{
"path": "examples/demo/index.html",
"chars": 723,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"/favicon.ico\" />\n <"
},
{
"path": "examples/demo/package.json",
"chars": 1033,
"preview": "{\n \"name\": \"demo\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \""
},
{
"path": "examples/demo/public/manifest.json",
"chars": 515,
"preview": "{\n \"short_name\": \"Zustand\",\n \"name\": \"🐻 Bear necessities for state management in React\",\n \"icons\": [\n {\n \"src"
},
{
"path": "examples/demo/public/robots.txt",
"chars": 67,
"preview": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
},
{
"path": "examples/demo/src/App.jsx",
"chars": 766,
"preview": "import { create } from 'zustand'\nimport CodePreview from './components/CodePreview'\nimport Details from './components/De"
},
{
"path": "examples/demo/src/components/CodePreview.jsx",
"chars": 1419,
"preview": "import { create } from 'zustand'\nimport { Highlight } from 'prism-react-renderer'\nimport CopyButton from './CopyButton'\n"
},
{
"path": "examples/demo/src/components/CopyButton.jsx",
"chars": 1329,
"preview": "import { useState, useCallback, useRef } from 'react'\nimport { copyToClipboard } from '../utils/copy-to-clipboard'\n\n/*\nI"
},
{
"path": "examples/demo/src/components/Details.jsx",
"chars": 781,
"preview": "export default function Details() {\n return (\n <>\n <nav className=\"nav\">\n <a href=\"https://zustand.docs."
},
{
"path": "examples/demo/src/components/Fireflies.jsx",
"chars": 1695,
"preview": "import { Vector3, CatmullRomCurve3 } from 'three'\nimport { useRef, useMemo } from 'react'\nimport { extend, useFrame } fr"
},
{
"path": "examples/demo/src/components/Scene.jsx",
"chars": 5997,
"preview": "import { Mesh, PlaneGeometry, Group, Vector3, MathUtils } from 'three'\nimport { useRef, useState, useLayoutEffect } from"
},
{
"path": "examples/demo/src/components/SnippetLang.jsx",
"chars": 317,
"preview": "export default function SnippetLang({ lang, setLang }) {\n return (\n <select\n className=\"snippet-lang\"\n val"
},
{
"path": "examples/demo/src/main.jsx",
"chars": 175,
"preview": "import { createRoot } from 'react-dom/client'\nimport './styles.css'\nimport './pmndrs.css'\nimport App from './App'\n\ncreat"
},
{
"path": "examples/demo/src/materials/layerMaterial.js",
"chars": 1456,
"preview": "import { shaderMaterial } from '@react-three/drei'\nimport { extend } from '@react-three/fiber'\n\n// This material takes c"
},
{
"path": "examples/demo/src/pmndrs.css",
"chars": 2297,
"preview": "/**\n * Pmndrs theme for JavaScript, CSS and HTML\n * Loosely based on https://marketplace.visualstudio.com/items?itemName"
},
{
"path": "examples/demo/src/resources/javascript-code.js",
"chars": 329,
"preview": "export default `import { create } from 'zustand'\n\nconst useStore = create((set) => ({\n count: 1,\n inc: () => set((stat"
},
{
"path": "examples/demo/src/resources/typescript-code.js",
"chars": 390,
"preview": "export default `import { create } from 'zustand'\n\ntype Store = {\n count: number\n inc: () => void\n}\n\nconst useStore = c"
},
{
"path": "examples/demo/src/styles.css",
"chars": 3620,
"preview": "* {\n box-sizing: border-box;\n}\n\nhtml,\nbody,\n#root {\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n -webkit"
},
{
"path": "examples/demo/src/utils/copy-to-clipboard.js",
"chars": 88,
"preview": "export const copyToClipboard = (str) => {\n return navigator.clipboard.writeText(str)\n}\n"
},
{
"path": "examples/demo/vite.config.js",
"chars": 167,
"preview": "import react from '@vitejs/plugin-react-swc'\nimport { defineConfig } from 'vite'\n\n// https://vitejs.dev/config/\nexport d"
},
{
"path": "examples/starter/README.md",
"chars": 581,
"preview": "# Starter [: obj is Iterable<unknown> =>\n Symbol.iterator in obj\n\nconst hasIterableEntries = (\n v"
},
{
"path": "src/vanilla.ts",
"chars": 3338,
"preview": "type SetStateInternal<T> = {\n _(\n partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],\n replace?: fals"
},
{
"path": "tests/basic.test.tsx",
"chars": 17582,
"preview": "import {\n Component as ClassComponent,\n StrictMode,\n useEffect,\n useLayoutEffect,\n useState,\n} from 'react'\nimport "
},
{
"path": "tests/devtools.test.tsx",
"chars": 88317,
"preview": "import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\nimport type { Mock } from 'vitest'\nimport { dev"
},
{
"path": "tests/middlewareTypes.test.tsx",
"chars": 28259,
"preview": "import { describe, expect, expectTypeOf, it } from 'vitest'\nimport { create } from 'zustand'\nimport type { StateCreator,"
},
{
"path": "tests/persistAsync.test.tsx",
"chars": 27961,
"preview": "/// <reference types=\"node\" />\n\nimport { StrictMode, useEffect } from 'react'\nimport { act, render, screen } from '@test"
},
{
"path": "tests/persistSync.test.tsx",
"chars": 20079,
"preview": "/// <reference types=\"node\" />\n\nimport { afterEach, describe, expect, it, vi } from 'vitest'\nimport { create } from 'zus"
},
{
"path": "tests/setup.ts",
"chars": 42,
"preview": "import '@testing-library/jest-dom/vitest'\n"
},
{
"path": "tests/shallow.test.tsx",
"chars": 5490,
"preview": "import { useState } from 'react'\nimport { act, fireEvent, render, screen } from '@testing-library/react'\nimport { before"
},
{
"path": "tests/ssr.test.tsx",
"chars": 2919,
"preview": "import React, { useEffect } from 'react'\nimport { act, screen } from '@testing-library/react'\nimport { renderToString } "
},
{
"path": "tests/subscribe.test.tsx",
"chars": 279,
"preview": "import { describe, expect, it } from 'vitest'\nimport { create } from 'zustand'\n\ndescribe('subscribe()', () => {\n it('sh"
},
{
"path": "tests/test-utils.ts",
"chars": 718,
"preview": "type ReplacedMap = {\n type: 'Map'\n value: [string, unknown][]\n}\n\nexport const replacer = (\n key: string,\n value: unk"
},
{
"path": "tests/types.test.tsx",
"chars": 6483,
"preview": "import { expect, it } from 'vitest'\nimport { create } from 'zustand'\nimport type {\n StateCreator,\n StoreApi,\n StoreMu"
},
{
"path": "tests/vanilla/basic.test.ts",
"chars": 3327,
"preview": "import { afterEach, expect, it, vi } from 'vitest'\nimport { createStore } from 'zustand/vanilla'\nimport type { StoreApi "
},
{
"path": "tests/vanilla/shallow.test.tsx",
"chars": 5871,
"preview": "import { describe, expect, it } from 'vitest'\nimport { shallow } from 'zustand/shallow'\n\ndescribe('shallow', () => {\n i"
},
{
"path": "tests/vanilla/subscribe.test.tsx",
"chars": 4401,
"preview": "import { describe, expect, it, vi } from 'vitest'\nimport { subscribeWithSelector } from 'zustand/middleware'\nimport { cr"
},
{
"path": "tsconfig.json",
"chars": 750,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"esModuleInterop\": true"
},
{
"path": "vitest.config.mts",
"chars": 817,
"preview": "import { resolve } from 'path'\nimport { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n resolve: {\n"
}
]
About this extraction
This page contains the full source code of the pmndrs/zustand GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 128 files (680.4 KB), approximately 181.5k tokens, and a symbol index with 216 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.