* prepare for the next major version

* [v5] breaking: drop default exports (#2238)

* fix: drop default exports for v5

* chore: remove default from cjs build

* refactor: export shallow in v5

* fix: remove `addModuleExport` option for cjs.

* [v5] breaking: drop deprecated features (#2235)

* fix: remove deprecated v4 features

* chore(build): remove context

* docs(typescript): remove deprecated equals api

* docs(persist): remove old persist api

* chore: run yarn prettier on typescript docs

* Discard changes to docs/guides/typescript.md

* Discard changes to docs/integrations/persisting-store-data.md

* Discard changes to tests/shallow.test.tsx

* Discard changes to tests/vanilla/subscribe.test.tsx

* [v5] breaking: make React 18 as minimal requirement (#2236)

* fix: update package.json to require react 18+

* chore: update github actions to test on react 18+

* chore: remove devtools-skip hack from actions

* chore(test): remove CI-SKIP from devtools tests

* [v5] breaking: make use-sync-external-store an optional peer dependency (#2237)

* chore: make use-sync-external-store optional peerDep

* fix: use correct versions in package.json

* [v5] breaking: require TypeScript 4.5 and update tests (#2257)

* breaking(types): TS requirement

* wip: latest only

* wip: latest only 2

* drop ts <4.4

* wip: do not skip lib checkes

* use latest node types

* drop ts 4.4

* [v5]: drop "module" condition  (#2270)

* Update package json in order to remove module

* Update rollup config in order to remove module config

* Update patch esm script

* Update package json to general exports and update node version (#2272)

* [v5]: drop UMD/SystemJS builds (#2287)

* Update rollup config in order to drop system js and umd builds

* Update packages

* Clean up files

* Update rollup config

* Update gh workflows

* Minor fixes

* Minor fixes

* Minor fixes

* Minor fixes

* Testing

* Minor changes

* Minor fixes

* remove `WithReact` type (#2300)

* 5.0.0-alpha.0

* [v5]: do not depend on use-sync-external-store (#2301)

* [v5]: do not depend on use-sync-external-store

* memo get(server)snapshot

* 5.0.0-alpha.1

* [v5]: refactor useMemoSelector (#2302)

* [v5]: refactor useMemoSelector

* add a test

* Revert "[v5]: refactor useMemoSelector"

This reverts commit b3c8b15586a270d12c335e566975021adf86c815.

* Revert "Revert "[v5]: refactor useMemoSelector""

This reverts commit 3c47301d23e18dffb7d72df36595f83570d15d08.

* [v5]: separate react entry point (#2303)

* 5.0.0-alpha.2

* 5.0.0-alpha.3

* refactor: Switch to Object.hasOwn (#2365)

* [v5] drop es5 (#2380)

* update yarn lock

* 5.0.0-alpha.4

* [v5]: follow React "standard" way with breaking behavioral change (#2395)

* [v5]: follow React "standard" way with breaking behavioral change

* add test

* 5.0.0-alpha.5

* [v5] Rewrite shallow to support iterables (#2427)

* [v5] fix rollup config for cjs (#2433)

* 5.0.0-alpha.6

* no production build test

* recover types that are dropped in #2462

* remove unused replacement

* [v5] Remove Devtools warning (#2466)

* chore: remove devtools extension warning

* docs: add devtools link to readme

* chore: remove unused test

* chrome: remove unused tests

* chore: remove unused test

* Revert "chore: remove unused test"

This reverts commit 0fa2a75f4936d960f703bf19e8f3505962cd628e.

* update test name

* update pnpm lock

* fix merge main

* add migration guide

* fix typos

* 5.0.0-beta.0

* update migration doc

* fix merge main

* fix merge main (prettier)

* 5.0.0-beta.1

* fix(types)!: require complete state if `setState`'s `replace` flag is set (#2580)

* fix(types): require complete state if `setState`'s `replace` flag is set

* switch to variant 2

* fix type errors

* update setState types for devtools and immer

* make devtools setState non-generic

* add migration guide

* merge migration guides

* run prettier

* Update tests/middlewareTypes.test.tsx

---------

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
Co-authored-by: daishi <daishi@axlight.com>

* 5.0.0-beta.2

* move v5 migration doc

* fix ci

* missing commmit

* remove unused rule exclusion

* comment about react compiler

* revert eslint config

---------

Co-authored-by: Charles Kornoelje <33156025+charkour@users.noreply.github.com>
Co-authored-by: Danilo Britto <dbritto.dev@gmail.com>
Co-authored-by: Ekin Dursun <ekindursun@gmail.com>
Co-authored-by: Simon Farshid <simon.farshid@outlook.com>
This commit is contained in:
Daishi Kato 2024-08-16 09:41:00 +09:00 committed by GitHub
parent 53e5a2a7f5
commit e247220ece
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 468 additions and 2505 deletions

View File

@ -45,11 +45,7 @@
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
],
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-empty-function": "off",

View File

@ -12,8 +12,8 @@ jobs:
strategy:
fail-fast: false
matrix:
build: [cjs, esm, umd]
env: [development, production]
build: [cjs, esm]
env: [development] # [development, production]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
@ -24,10 +24,6 @@ jobs:
cache-dependency-path: '**/pnpm-lock.yaml'
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Use React 17 for production test
if: ${{ matrix.env == 'production' }}
run: |
pnpm add -D react@17.0.2 react-dom@17.0.2 @testing-library/react@12.1.4
- name: Patch for DEV-ONLY
if: ${{ matrix.env == 'development' }}
run: |
@ -42,7 +38,6 @@ jobs:
if: ${{ matrix.build == 'cjs' }}
run: |
sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\1.js')/" vitest.config.ts
sed -i~ "s/module.exports.createStore = vanilla.createStore;//" dist/index.js
- name: Patch for ESM
if: ${{ matrix.build == 'esm' }}
run: |
@ -50,18 +45,6 @@ jobs:
sed -i~ "1s/^/import.meta.env=import.meta.env||{};import.meta.env.MODE='${NODE_ENV}';/" tests/*.tsx
env:
NODE_ENV: ${{ matrix.env }}
- name: Patch for UMD
if: ${{ matrix.build == 'umd' }}
run: |
sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\/umd\1.${NODE_ENV}.js')/" vitest.config.ts
env:
NODE_ENV: ${{ matrix.env }}
- name: Patch for SystemJS
if: ${{ matrix.build == 'system' }}
run: |
sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\/system\1.${NODE_ENV}.js')/" vitest.config.ts
env:
NODE_ENV: ${{ matrix.env }}
- name: Test ${{ matrix.build }} ${{ matrix.env }}
run: |
pnpm test:spec

View File

@ -27,8 +27,6 @@ jobs:
fail-fast: false
matrix:
react:
- 16.8.0
- 17.0.0
- 18.0.0
- 18.1.0
- 18.2.0
@ -36,22 +34,6 @@ jobs:
- 19.0.0-rc.0
- 19.0.0-rc-49496d49-20240814
- 0.0.0-experimental-49496d49-20240814
devtools-skip:
- CI-MATRIX-NOSKIP
include:
- devtools-skip: CI-MATRIX-[2345]
react: 16.8.0
- devtools-skip: CI-MATRIX-[1345]
react: 16.8.0
- devtools-skip: CI-MATRIX-[1245]
react: 16.8.0
- devtools-skip: CI-MATRIX-[1235]
react: 16.8.0
- devtools-skip: CI-MATRIX-[1234]
react: 16.8.0
exclude:
- devtools-skip: CI-MATRIX-NOSKIP
react: 16.8.0
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
@ -61,18 +43,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- run: pnpm install --frozen-lockfile
- name: Install legacy testing-library
if: ${{ startsWith(matrix.react, '16.') || startsWith(matrix.react, '17.') }}
run: pnpm add -D @testing-library/react@12.1.4
- name: Patch for React 16
if: ${{ startsWith(matrix.react, '16.') }}
run: |
sed -i~ '1s/^/import React from "react";/' tests/*.tsx
sed -i~ 's/"jsx": "react-jsx"/"jsx": "react"/' tsconfig.json
sed -i~ 's/import\.meta\.env[?]\.MODE/"DEVELOPMENT".toLowerCase()/' src/*.ts src/*/*.ts
sed -i~ "s/it('\[${DEVTOOLS_SKIP}\]/it.skip('/" tests/devtools.test.tsx
env:
DEVTOOLS_SKIP: ${{ matrix.devtools-skip }}
- name: Test ${{ matrix.react }} ${{ matrix.devtools-skip }}
run: |
pnpm add -D react@${{ matrix.react }} react-dom@${{ matrix.react }}

View File

@ -23,10 +23,6 @@ jobs:
- 4.7.4
- 4.6.4
- 4.5.5
- 4.4.4
- 4.3.5
- 4.2.3
- 4.1.5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
@ -43,9 +39,6 @@ jobs:
sed -i~ 's/"verbatimModuleSyntax": true,//' tsconfig.json
- name: Patch for Old TS
run: |
sed -i~ 's/\/\/ @ts-expect-error.*\[LATEST-TS-ONLY\]//' tests/*.tsx
sed -i~ 's/"target":/"skipLibCheck":true,"target":/' tsconfig.json
sed -i~ 's/"exactOptionalPropertyTypes": true,//' tsconfig.json
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
@ -55,9 +48,5 @@ jobs:
pnpm add -D @types/node@18.13.0
- name: Install old TypeScript
run: pnpm add -D typescript@${{ matrix.typescript }}
- name: Patch testing setup for Old TS
if: ${{ matrix.typescript == '4.4.4' || matrix.typescript == '4.3.5' || matrix.typescript == '4.2.3' || matrix.typescript == '4.1.5' }}
run: |
pnpm add -D vitest@0.33.0 @vitest/coverage-v8@0.33.0 @vitest/ui@0.33.0
- name: Test ${{ matrix.typescript }}
run: pnpm test:types

View File

@ -1,28 +0,0 @@
module.exports = (api, targets) => {
// https://babeljs.io/docs/en/config-files#config-function-api
const isTestEnv = api.env('test')
return {
babelrc: false,
ignore: ['./node_modules'],
presets: [
[
'@babel/preset-env',
{
loose: true,
modules: isTestEnv ? 'commonjs' : false,
targets: isTestEnv ? { node: 'current' } : targets,
},
],
],
plugins: [
[
'@babel/plugin-transform-react-jsx',
{
runtime: 'automatic',
},
],
['@babel/plugin-transform-typescript', { isTSX: true }],
],
}
}

View File

@ -232,6 +232,36 @@ For a usual statically typed language, this is impossible. But thanks to TypeScr
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 `as any`:
```ts
const replaceFlag = Math.random() > 0.5
store.setState(partialOrFull, replaceFlag as any)
```
#### Example with `as any` 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
useBearStore.setState({ bears: 5 }, replaceFlag as any) // 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

View File

@ -0,0 +1,171 @@
---
title: 'How to Migrate to v5 from v4'
nav: 30
---
# How to Migrate to v5 from v4
We highly recommend to update to the latest version of v4, before migrating to v5. It will show all deprecation warnings without breaking your app.
## Changes in v5
- Drop default exports
- Drop deprecated features
- Make React 18 the minimum required version
- Make use-sync-external-store a peer dependency (required for `createWithEqualityFn` and `useStoreWithEqualityFn` in `zustand/traditional`)
- Make TypeScript 4.5 the minimum required version
- Drop UMD/SystemJS support
- Organize entry points in the package.json
- Drop ES5 support
- Stricter types when setState's replace flag is set
- Other small improvements (technically breaking changes)
## Migration Guide
### Using custom equality functions such as `shallow`
The `create` function in v5 does not support customizing equality function.
If you use custom equality function such as `shallow`,
the easiest migration is to use `createWithEqualityFn`.
```js
// v4
import { create } from 'zustand'
import { shallow } from 'zustand/shallow'
const useCountStore = create((set) => ({
count: 0,
text: 'hello',
// ...
}))
const Component = () => {
const { count, text } = useCountStore(
(state) => ({
count: state.count,
text: state.text,
}),
shallow,
)
// ...
}
```
That can be done with `createWithEqualityFn` in v5:
```bash
npm install use-sync-external-store
```
```js
// v5
import { createWithEqualityFn as create } from 'zustand/traditional'
// The rest is the same as v4
```
Alternatively, for the `shallow` use case, you can use `useShallow` hook:
```js
// v5
import { create } from 'zustand'
import { useShallow } from 'zustand/shallow'
const useCountStore = create((set) => ({
count: 0,
text: 'hello',
// ...
}))
const Component = () => {
const { count, text } = useCountStore(
useShallow((state) => ({
count: state.count,
text: state.text,
})),
)
// ...
}
```
### Requiring stable selector outputs
There is a behavioral change in v5 to match React default behavior.
If a selector returns a new reference, it may cause infinite loops.
For example, this may cause infinite loops.
```js
// v4
const action = useMainStore((state) => {
return state.action ?? () => {}
})
```
The error message will be something like this:
```
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
```
To fix it, make sure the selector function returns a stable reference.
```js
// v5
const FALLBACK_ACTION = () => {}
const action = useMainStore((state) => {
return state.action ?? FALLBACK_ACTION
})
```
Alternatively, if you need v4 behavior, `createWithEqualityFn` will do.
```js
// v5
import { createWithEqualityFn as create } from 'zustand/traditional'
```
### Stricter types when setState's replace flag is set (Typescript only)
```diff
- setState:
- (partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: boolean | undefined) => void;
+ setState:
+ (partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false) => void;
+ (state: T | ((state: T) => T), replace: true) => void;
```
If you are not using the `replace` flag, no migration is required.
If you are using the `replace` flag and it's set to `true`, you must provide a complete state object.
This change ensures that `store.setState({}, true)` (which results in an invalid state) is no longer considered valid.
**Examples:**
```ts
// Partial state update (valid)
store.setState({ key: 'value' })
// Complete state replacement (valid)
store.setState({ key: 'value' }, true)
// Incomplete state replacement (invalid)
store.setState({}, true) // Error
```
#### Handling Dynamic `replace` Flag
If the value of the `replace` flag is dynamic and determined at runtime, you might face issues. To handle this, you can use a workaround by annotating the `replace` parameter with `as any`:
```ts
const replaceFlag = Math.random() > 0.5
store.setState(partialOrFull, replaceFlag as any)
```
## Links
- https://github.com/pmndrs/zustand/pull/2138
- https://github.com/pmndrs/zustand/pull/2580

View File

@ -2,22 +2,31 @@
"name": "zustand",
"description": "🐻 Bear necessities for state management in React",
"private": true,
"version": "4.5.5",
"type": "commonjs",
"version": "5.0.0-beta.2",
"publishConfig": {
"tag": "next"
},
"main": "./index.js",
"types": "./index.d.ts",
"typesVersions": {
"<4.0": {
">=4.5": {
"esm/*": [
"ts3.4/*"
"esm/*"
],
"*": [
"ts3.4/*"
"*"
]
},
"*": {
"esm/*": [
"ts_version_4.5_and_above_is_required.d.ts"
],
"*": [
"ts_version_4.5_and_above_is_required.d.ts"
]
}
},
"files": [
"**"
],
"exports": {
"./package.json": "./package.json",
".": {
@ -25,142 +34,39 @@
"types": "./esm/index.d.mts",
"default": "./esm/index.mjs"
},
"module": {
"types": "./esm/index.d.ts",
"default": "./esm/index.js"
},
"default": {
"types": "./index.d.ts",
"default": "./index.js"
}
},
"./vanilla": {
"./*": {
"import": {
"types": "./esm/vanilla.d.mts",
"default": "./esm/vanilla.mjs"
},
"module": {
"types": "./esm/vanilla.d.ts",
"default": "./esm/vanilla.js"
"types": "./esm/*.d.mts",
"default": "./esm/*.mjs"
},
"default": {
"types": "./vanilla.d.ts",
"default": "./vanilla.js"
}
},
"./middleware": {
"import": {
"types": "./esm/middleware.d.mts",
"default": "./esm/middleware.mjs"
},
"module": {
"types": "./esm/middleware.d.ts",
"default": "./esm/middleware.js"
},
"default": {
"types": "./middleware.d.ts",
"default": "./middleware.js"
}
},
"./middleware/immer": {
"import": {
"types": "./esm/middleware/immer.d.mts",
"default": "./esm/middleware/immer.mjs"
},
"module": {
"types": "./esm/middleware/immer.d.ts",
"default": "./esm/middleware/immer.js"
},
"default": {
"types": "./middleware/immer.d.ts",
"default": "./middleware/immer.js"
}
},
"./shallow": {
"import": {
"types": "./esm/shallow.d.mts",
"default": "./esm/shallow.mjs"
},
"module": {
"types": "./esm/shallow.d.ts",
"default": "./esm/shallow.js"
},
"default": {
"types": "./shallow.d.ts",
"default": "./shallow.js"
}
},
"./vanilla/shallow": {
"import": {
"types": "./esm/vanilla/shallow.d.mts",
"default": "./esm/vanilla/shallow.mjs"
},
"module": {
"types": "./esm/vanilla/shallow.d.ts",
"default": "./esm/vanilla/shallow.js"
},
"default": {
"types": "./vanilla/shallow.d.ts",
"default": "./vanilla/shallow.js"
}
},
"./react/shallow": {
"import": {
"types": "./esm/react/shallow.d.mts",
"default": "./esm/react/shallow.mjs"
},
"module": {
"types": "./esm/react/shallow.d.ts",
"default": "./esm/react/shallow.js"
},
"default": {
"types": "./react/shallow.d.ts",
"default": "./react/shallow.js"
}
},
"./traditional": {
"import": {
"types": "./esm/traditional.d.mts",
"default": "./esm/traditional.mjs"
},
"module": {
"types": "./esm/traditional.d.ts",
"default": "./esm/traditional.js"
},
"default": {
"types": "./traditional.d.ts",
"default": "./traditional.js"
}
},
"./context": {
"import": {
"types": "./esm/context.d.mts",
"default": "./esm/context.mjs"
},
"module": {
"types": "./esm/context.d.ts",
"default": "./esm/context.js"
},
"default": {
"types": "./context.d.ts",
"default": "./context.js"
"types": "./*.d.ts",
"default": "./*.js"
}
}
},
"files": [
"**"
],
"sideEffects": false,
"scripts": {
"prebuild": "shx rm -rf dist",
"build": "pnpm run prebuild && pnpm run '/^build:.*/' && pnpm run postbuild",
"build:base": "rollup -c",
"build:vanilla": "rollup -c --config-vanilla",
"build:react": "rollup -c --config-react",
"build:middleware": "rollup -c --config-middleware",
"build:middleware:immer": "rollup -c --config-middleware_immer",
"build:shallow": "rollup -c --config-shallow",
"build:vanilla:shallow": "rollup -c --config-vanilla_shallow",
"build:react:shallow": "rollup -c --config-react_shallow",
"build:traditional": "rollup -c --config-traditional",
"build:context": "rollup -c --config-context",
"postbuild": "pnpm patch-d-ts && pnpm copy && pnpm patch-esm-ts",
"postbuild": "pnpm patch-d-ts && pnpm copy && pnpm patch-old-ts && pnpm patch-esm-ts",
"prettier": "prettier \"*.{js,json,md}\" \"{examples,src,tests,docs}/**/*.{js,jsx,ts,tsx,md,mdx}\" --write",
"eslint": "eslint --no-eslintrc --c .eslintrc.json --fix '*.{js,ts}' '{src,tests}/**/*.{ts,tsx}'",
"test": "pnpm run '/^test:.*/'",
@ -169,11 +75,12 @@
"test:lint": "eslint --no-eslintrc --c .eslintrc.json '*.{js,ts}' '{src,tests}/**/*.{ts,tsx}'",
"test:spec": "vitest run",
"patch-d-ts": "node -e \"var {entries}=require('./rollup.config.js');require('shelljs').find('dist/**/*.d.ts').forEach(f=>{entries.forEach(({find,replacement})=>require('shelljs').sed('-i',new RegExp(' from \\''+find.source.slice(0,-1)+'\\';$'),' from \\''+replacement+'\\';',f));require('shelljs').sed('-i',/ from '(\\.[^']+)\\.ts';$/,' from \\'\\$1\\';',f)})\"",
"copy": "shx cp -r dist/src/* dist/esm && shx cp -r dist/src/* dist && shx rm -rf dist/src && shx rm -rf dist/{src,tests} && downlevel-dts dist dist/ts3.4 && shx cp package.json readme.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.prettier=undefined;\"",
"patch-esm-ts": "node -e \"require('shelljs').find('dist/esm/**/*.d.ts').forEach(f=>{var f2=f.replace(/\\.ts$/,'.mts');require('fs').copyFileSync(f,f2);require('shelljs').sed('-i',/ from '(\\.[^']+)';$/,' from \\'\\$1.mjs\\';',f2);require('shelljs').sed('-i',/^declare module '(\\.[^']+)'/,'declare module \\'\\$1.mjs\\'',f2)})\""
"copy": "shx cp -r dist/src/* dist/esm && shx cp -r dist/src/* dist && shx rm -rf dist/src && shx rm -rf dist/{src,tests} && shx cp package.json readme.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.prettier=undefined;\"",
"patch-old-ts": "shx touch dist/ts_version_4.5_and_above_is_required.d.ts",
"patch-esm-ts": "node -e \"require('shelljs').find('dist/esm/**/*.d.ts').forEach(f=>{var f2=f.replace(/\\.ts$/,'.mts');require('fs').renameSync(f,f2);require('shelljs').sed('-i',/ from '(\\.[^']+)';$/,' from \\'\\$1.mjs\\';',f2);require('shelljs').sed('-i',/^declare module '(\\.[^']+)'/,'declare module \\'\\$1.mjs\\'',f2)})\""
},
"engines": {
"node": ">=12.7.0"
"node": ">=12.20.0"
},
"prettier": {
"semi": false,
@ -202,22 +109,11 @@
},
"homepage": "https://github.com/pmndrs/zustand",
"packageManager": "pnpm@8.15.0",
"dependencies": {
"use-sync-external-store": "1.2.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/plugin-external-helpers": "^7.24.7",
"@babel/plugin-transform-react-jsx": "^7.25.2",
"@babel/plugin-transform-runtime": "^7.24.7",
"@babel/plugin-transform-typescript": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@redux-devtools/extension": "^3.3.0",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.7",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
"@testing-library/react": "^16.0.0",
"@types/node": "^22.3.0",
@ -228,7 +124,6 @@
"@typescript-eslint/parser": "^8.1.0",
"@vitest/coverage-v8": "^2.0.5",
"@vitest/ui": "^2.0.5",
"downlevel-dts": "^0.11.0",
"esbuild": "^0.23.0",
"eslint": "8.57.0",
"eslint-config-prettier": "^9.1.0",
@ -251,12 +146,14 @@
"shelljs": "^0.8.5",
"shx": "^0.3.4",
"typescript": "^5.5.4",
"use-sync-external-store": "^1.2.2",
"vitest": "^2.0.5"
},
"peerDependencies": {
"@types/react": ">=16.8",
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=16.8"
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
@ -267,6 +164,9 @@
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
}

1358
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -360,6 +360,8 @@ 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'

View File

@ -1,33 +1,25 @@
const path = require('path')
const alias = require('@rollup/plugin-alias')
const babelPlugin = require('@rollup/plugin-babel')
const resolve = require('@rollup/plugin-node-resolve')
const replace = require('@rollup/plugin-replace')
const terser = require('@rollup/plugin-terser')
const typescript = require('@rollup/plugin-typescript')
const { default: esbuild } = require('rollup-plugin-esbuild')
const createBabelConfig = require('./babel.config.js')
const extensions = ['.js', '.ts', '.tsx']
const { root } = path.parse(process.cwd())
const entries = [{ find: /.*\/vanilla\.ts$/, replacement: 'zustand/vanilla' }]
const entries = [
{ find: /.*\/vanilla\/shallow\.ts$/, replacement: 'zustand/vanilla/shallow' },
{ find: /.*\/react\/shallow\.ts$/, replacement: 'zustand/react/shallow' },
{ find: /.*\/vanilla\.ts$/, replacement: 'zustand/vanilla' },
{ find: /.*\/react\.ts$/, replacement: 'zustand/react' },
]
function external(id) {
return !id.startsWith('.') && !id.startsWith(root)
}
function getBabelOptions(targets) {
return {
...createBabelConfig({ env: (env) => env === 'build' }, targets),
extensions,
comments: false,
babelHelpers: 'bundled',
}
}
function getEsbuild(env = 'development') {
function getEsbuild() {
return esbuild({
minify: env === 'production',
target: 'es2018',
supported: { 'import-meta': true },
tsconfig: path.resolve('./tsconfig.json'),
@ -68,7 +60,7 @@ function createESMConfig(input, output) {
'import.meta.env?.MODE':
'(import.meta.env ? import.meta.env.MODE : undefined)',
}),
// a workround for #829
// a workaround for #829
'use-sync-external-store/shim/with-selector':
'use-sync-external-store/shim/with-selector.js',
delimiters: ['\\b', '\\b(?!(\\.|/))'],
@ -79,23 +71,10 @@ function createESMConfig(input, output) {
}
}
function createCommonJSConfig(input, output, options) {
function createCommonJSConfig(input, output) {
return {
input,
output: {
file: `${output}.js`,
format: 'cjs',
esModule: false,
outro: options.addModuleExport
? [
`module.exports = ${options.addModuleExport.default};`,
...Object.entries(options.addModuleExport)
.filter(([key]) => key !== 'default')
.map(([key, value]) => `module.exports.${key} = ${value};`),
`exports.default = module.exports;`,
].join('\n')
: '',
},
output: { file: output, format: 'cjs' },
external,
plugins: [
alias({ entries: entries.filter((e) => !e.find.test(input)) }),
@ -105,65 +84,7 @@ function createCommonJSConfig(input, output, options) {
delimiters: ['\\b', '\\b(?!(\\.|/))'],
preventAssignment: true,
}),
babelPlugin(getBabelOptions({ ie: 11 })),
],
}
}
function createUMDConfig(input, output, env) {
let name = 'zustand'
const fileName = output.slice('dist/umd/'.length)
const capitalize = (s) => s.slice(0, 1).toUpperCase() + s.slice(1)
if (fileName !== 'index') {
name += fileName.replace(/(\w+)\W*/g, (_, p) => capitalize(p))
}
return {
input,
output: {
file: `${output}.${env}.js`,
format: 'umd',
name,
globals: {
react: 'React',
immer: 'immer',
// FIXME not yet supported
'use-sync-external-store/shim/with-selector':
'useSyncExternalStoreShimWithSelector',
'zustand/vanilla': 'zustandVanilla',
},
},
external,
plugins: [
alias({ entries: entries.filter((e) => !e.find.test(input)) }),
resolve({ extensions }),
replace({
'import.meta.env?.MODE': JSON.stringify(env),
delimiters: ['\\b', '\\b(?!(\\.|/))'],
preventAssignment: true,
}),
babelPlugin(getBabelOptions({ ie: 11 })),
...(env === 'production' ? [terser()] : []),
],
}
}
function createSystemConfig(input, output, env) {
return {
input,
output: {
file: `${output}.${env}.js`,
format: 'system',
},
external,
plugins: [
alias({ entries: entries.filter((e) => !e.find.test(input)) }),
resolve({ extensions }),
replace({
'import.meta.env?.MODE': JSON.stringify(env),
delimiters: ['\\b', '\\b(?!(\\.|/))'],
preventAssignment: true,
}),
getEsbuild(env),
getEsbuild(),
],
}
}
@ -177,24 +98,8 @@ module.exports = function (args) {
}
return [
...(c === 'index' ? [createDeclarationConfig(`src/${c}.ts`, 'dist')] : []),
createCommonJSConfig(`src/${c}.ts`, `dist/${c}`, {
addModuleExport: {
index: {
default: 'react',
create: 'create',
useStore: 'useStore',
createStore: 'vanilla.createStore',
},
vanilla: { default: 'vanilla', createStore: 'createStore' },
shallow: { default: 'shallow', shallow: 'shallow$1' },
}[c],
}),
createESMConfig(`src/${c}.ts`, `dist/esm/${c}.js`),
createCommonJSConfig(`src/${c}.ts`, `dist/${c}.js`),
createESMConfig(`src/${c}.ts`, `dist/esm/${c}.mjs`),
createUMDConfig(`src/${c}.ts`, `dist/umd/${c}`, 'development'),
createUMDConfig(`src/${c}.ts`, `dist/umd/${c}`, 'production'),
createSystemConfig(`src/${c}.ts`, `dist/system/${c}`, 'development'),
createSystemConfig(`src/${c}.ts`, `dist/system/${c}`, 'production'),
]
}

View File

@ -1,102 +0,0 @@
// import {
// createElement,
// createContext as reactCreateContext,
// useContext,
// useMemo,
// useRef,
// } from 'react'
// That doesnt work in ESM, because React libs are CJS only.
// The following is a workaround until ESM is supported.
// eslint-disable-next-line import/extensions
import ReactExports from 'react'
import type { ReactNode } from 'react'
import type { StoreApi } from 'zustand'
// eslint-disable-next-line import/extensions
import { useStoreWithEqualityFn } from 'zustand/traditional'
const {
createElement,
createContext: reactCreateContext,
useContext,
useMemo,
useRef,
} = ReactExports
type UseContextStore<S extends StoreApi<unknown>> = {
(): ExtractState<S>
<U>(
selector: (state: ExtractState<S>) => U,
equalityFn?: (a: U, b: U) => boolean,
): U
}
type ExtractState<S> = S extends { getState: () => infer T } ? T : never
type WithoutCallSignature<T> = { [K in keyof T]: T[K] }
/**
* @deprecated Use `createStore` and `useStore` for context usage
*/
function createContext<S extends StoreApi<unknown>>() {
if (import.meta.env?.MODE !== 'production') {
console.warn(
"[DEPRECATED] `context` will be removed in a future version. Instead use `import { createStore, useStore } from 'zustand'`. See: https://github.com/pmndrs/zustand/discussions/1180.",
)
}
const ZustandContext = reactCreateContext<S | undefined>(undefined)
const Provider = ({
createStore,
children,
}: {
createStore: () => S
children: ReactNode
}) => {
const storeRef = useRef<S>()
if (!storeRef.current) {
storeRef.current = createStore()
}
return createElement(
ZustandContext.Provider,
{ value: storeRef.current },
children,
)
}
const useContextStore: UseContextStore<S> = <StateSlice = ExtractState<S>>(
selector?: (state: ExtractState<S>) => StateSlice,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean,
) => {
const store = useContext(ZustandContext)
if (!store) {
throw new Error(
'Seems like you have not used zustand provider as an ancestor.',
)
}
return useStoreWithEqualityFn(
store,
selector as (state: ExtractState<S>) => StateSlice,
equalityFn,
)
}
const useStoreApi = () => {
const store = useContext(ZustandContext)
if (!store) {
throw new Error(
'Seems like you have not used zustand provider as an ancestor.',
)
}
return useMemo<WithoutCallSignature<S>>(() => ({ ...store }), [store])
}
return {
Provider,
useStore: useContextStore,
useStoreApi,
}
}
export default createContext

View File

@ -1,3 +1,2 @@
export * from './vanilla.ts'
export * from './react.ts'
export { default } from './react.ts'

View File

@ -49,13 +49,22 @@ type TakeTwo<T> = T extends { length: 0 }
type WithDevtools<S> = Write<S, StoreDevtools<S>>
type Action =
| string
| {
type: string
[x: string | number | symbol]: unknown
}
type StoreDevtools<S> = S extends {
setState: (...a: infer Sa) => infer Sr
setState: {
// capture both overloads of setState
(...a: infer Sa1): infer Sr1
(...a: infer Sa2): infer Sr2
}
}
? {
setState<A extends string | { type: string }>(
...a: [...a: TakeTwo<Sa>, action?: A]
): Sr
setState(...a: [...a: TakeTwo<Sa1>, action?: Action]): Sr1
setState(...a: [...a: TakeTwo<Sa2>, action?: Action]): Sr2
}
: never
@ -153,16 +162,11 @@ const devtoolsImpl: DevtoolsImpl =
extensionConnector =
(enabled ?? import.meta.env?.MODE !== 'production') &&
window.__REDUX_DEVTOOLS_EXTENSION__
} catch (_e) {
} catch {
// ignored
}
if (!extensionConnector) {
if (import.meta.env?.MODE !== 'production' && enabled) {
console.warn(
'[zustand devtools middleware] Please install/enable Redux devtools extension',
)
}
return fn(set, get, api)
}
@ -170,8 +174,8 @@ const devtoolsImpl: DevtoolsImpl =
extractConnectionInformation(store, extensionConnector, options)
let isRecording = true
;(api.setState as NamedSet<S>) = (state, replace, nameOrAction) => {
const r = set(state, replace)
;(api.setState as any) = ((state, replace, nameOrAction: Action) => {
const r = set(state, replace as any)
if (!isRecording) return r
const action: { type: string } =
nameOrAction === undefined
@ -194,12 +198,12 @@ const devtoolsImpl: DevtoolsImpl =
},
)
return r
}
}) as NamedSet<S>
const setStateFromDevtools: StoreApi<S>['setState'] = (...a) => {
const originalIsRecording = isRecording
isRecording = false
set(...a)
set(...(a as Parameters<typeof set>))
isRecording = originalIsRecording
}
@ -269,7 +273,7 @@ const devtoolsImpl: DevtoolsImpl =
if (Object.keys(action.state as S).length !== 1) {
console.error(
`
[zustand devtools middleware] Unsupported __setState action format.
[zustand devtools middleware] Unsupported __setState action format.
When using 'store' option in devtools(), the 'state' should have only one key, which is a value of 'store' that was passed in devtools(),
and value of this only key should be a state object. Example: { "type": "__setState", "state": { "abc123Store": { "foo": "bar" } } }
`,

View File

@ -38,13 +38,21 @@ type StoreImmer<S> = S extends {
getState: () => infer T
setState: infer SetState
}
? SetState extends (...a: infer A) => infer Sr
? SetState extends {
(...a: infer A1): infer Sr1
(...a: infer A2): infer Sr2
}
? {
setState(
nextStateOrUpdater: T | Partial<T> | ((state: Draft<T>) => void),
shouldReplace?: boolean | undefined,
...a: SkipTwo<A>
): Sr
shouldReplace?: false,
...a: SkipTwo<A1>
): Sr1
setState(
nextStateOrUpdater: T | ((state: Draft<T>) => void),
shouldReplace: true,
...a: SkipTwo<A2>
): Sr2
}
: never
: never
@ -61,7 +69,7 @@ const immerImpl: ImmerImpl = (initializer) => (set, get, store) => {
typeof updater === 'function' ? produce(updater as any) : updater
) as ((s: T) => T) | T | Partial<T>
return set(nextState as any, replace, ...a)
return set(nextState, replace as any, ...a)
}
return initializer(store.setState, get, store)

View File

@ -35,7 +35,7 @@ export function createJSONStorage<S>(
let storage: StateStorage | undefined
try {
storage = getStorage()
} catch (_e) {
} catch {
// prevent error if the storage is not defined (e.g. when server side rendering a page)
return
}
@ -66,34 +66,6 @@ export function createJSONStorage<S>(
export interface PersistOptions<S, PersistedState = S> {
/** Name of the storage (must be unique) */
name: string
/**
* @deprecated Use `storage` instead.
* A function returning a storage.
* The storage must fit `window.localStorage`'s api (or an async version of it).
* For example the storage could be `AsyncStorage` from React Native.
*
* @default () => localStorage
*/
getStorage?: () => StateStorage
/**
* @deprecated Use `storage` instead.
* Use a custom serializer.
* The returned string will be stored in the storage.
*
* @default JSON.stringify
*/
serialize?: (state: StorageValue<S>) => string | Promise<string>
/**
* @deprecated Use `storage` instead.
* Use a custom deserializer.
* Must return an object matching StorageValue<S>
*
* @param str The storage's current value.
* @default JSON.parse
*/
deserialize?: (
str: string,
) => StorageValue<PersistedState> | Promise<StorageValue<PersistedState>>
/**
* Use a custom persist storage.
*
@ -200,180 +172,7 @@ const toThenable =
}
}
const oldImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
type S = ReturnType<typeof config>
let options = {
getStorage: () => localStorage,
serialize: JSON.stringify as (state: StorageValue<S>) => string,
deserialize: JSON.parse as (str: string) => StorageValue<S>,
partialize: (state: S) => state,
version: 0,
merge: (persistedState: unknown, currentState: S) => ({
...currentState,
...(persistedState as object),
}),
...baseOptions,
}
let hasHydrated = false
const hydrationListeners = new Set<PersistListener<S>>()
const finishHydrationListeners = new Set<PersistListener<S>>()
let storage: StateStorage | undefined
try {
storage = options.getStorage()
} catch (_e) {
// prevent error if the storage is not defined (e.g. when server side rendering a page)
}
if (!storage) {
return config(
(...args) => {
console.warn(
`[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`,
)
set(...args)
},
get,
api,
)
}
const thenableSerialize = toThenable(options.serialize)
const setItem = (): Thenable<void> => {
const state = options.partialize({ ...get() })
let errorInSync: Error | undefined
const thenable = thenableSerialize({ state, version: options.version })
.then((serializedValue) =>
(storage as StateStorage).setItem(options.name, serializedValue),
)
.catch((e) => {
errorInSync = e
})
if (errorInSync) {
throw errorInSync
}
return thenable
}
const savedSetState = api.setState
api.setState = (state, replace) => {
savedSetState(state, replace)
void setItem()
}
const configResult = config(
(...args) => {
set(...args)
void setItem()
},
get,
api,
)
// a workaround to solve the issue of not storing rehydrated state in sync storage
// the set(state) value would be later overridden with initial state by create()
// to avoid this, we merge the state from localStorage into the initial state.
let stateFromStorage: S | undefined
// rehydrate initial state with existing stored state
const hydrate = () => {
if (!storage) return
hasHydrated = false
hydrationListeners.forEach((cb) => cb(get()))
const postRehydrationCallback =
options.onRehydrateStorage?.(get()) || undefined
// bind is used to avoid `TypeError: Illegal invocation` error
return toThenable(storage.getItem.bind(storage))(options.name)
.then((storageValue) => {
if (storageValue) {
return options.deserialize(storageValue)
}
})
.then((deserializedStorageValue) => {
if (deserializedStorageValue) {
if (
typeof deserializedStorageValue.version === 'number' &&
deserializedStorageValue.version !== options.version
) {
if (options.migrate) {
return options.migrate(
deserializedStorageValue.state,
deserializedStorageValue.version,
)
}
console.error(
`State loaded from storage couldn't be migrated since no migrate function was provided`,
)
} else {
return deserializedStorageValue.state
}
}
})
.then((migratedState) => {
stateFromStorage = options.merge(
migratedState as S,
get() ?? configResult,
)
set(stateFromStorage as S, true)
return setItem()
})
.then(() => {
postRehydrationCallback?.(stateFromStorage, undefined)
hasHydrated = true
finishHydrationListeners.forEach((cb) => cb(stateFromStorage as S))
})
.catch((e: Error) => {
postRehydrationCallback?.(undefined, e)
})
}
;(api as StoreApi<S> & StorePersist<S, S>).persist = {
setOptions: (newOptions) => {
options = {
...options,
...newOptions,
}
if (newOptions.getStorage) {
storage = newOptions.getStorage()
}
},
clearStorage: () => {
storage?.removeItem(options.name)
},
getOptions: () => options,
rehydrate: () => hydrate() as Promise<void>,
hasHydrated: () => hasHydrated,
onHydrate: (cb) => {
hydrationListeners.add(cb)
return () => {
hydrationListeners.delete(cb)
}
},
onFinishHydration: (cb) => {
finishHydrationListeners.add(cb)
return () => {
finishHydrationListeners.delete(cb)
}
},
}
hydrate()
return stateFromStorage || configResult
}
const newImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
type S = ReturnType<typeof config>
let options = {
storage: createJSONStorage<S>(() => localStorage),
@ -397,7 +196,7 @@ const newImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
console.warn(
`[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`,
)
set(...args)
set(...(args as Parameters<typeof set>))
},
get,
api,
@ -415,13 +214,13 @@ const newImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
const savedSetState = api.setState
api.setState = (state, replace) => {
savedSetState(state, replace)
savedSetState(state, replace as any)
void setItem()
}
const configResult = config(
(...args) => {
set(...args)
set(...(args as Parameters<typeof set>))
void setItem()
},
get,
@ -550,22 +349,6 @@ const newImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
return stateFromStorage || configResult
}
const persistImpl: PersistImpl = (config, baseOptions) => {
if (
'getStorage' in baseOptions ||
'serialize' in baseOptions ||
'deserialize' in baseOptions
) {
if (import.meta.env?.MODE !== 'production') {
console.warn(
'[DEPRECATED] `getStorage`, `serialize` and `deserialize` options are deprecated. Use `storage` option instead.',
)
}
return oldImpl(config, baseOptions)
}
return newImpl(config, baseOptions)
}
type Persist = <
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],

View File

@ -1,12 +1,8 @@
// import { useDebugValue } from 'react'
// import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
// Those don't work in ESM, because React libs are CJS only.
// import { useDebugValue, useSyncExternalStore } from 'react'
// That doesn't work in ESM, because React libs are CJS only.
// See: https://github.com/pmndrs/valtio/issues/452
// The following is a workaround until ESM is supported.
// eslint-disable-next-line import/extensions
import ReactExports from 'react'
// eslint-disable-next-line import/extensions
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector'
import { createStore } from './vanilla.ts'
import type {
Mutate,
@ -15,8 +11,7 @@ import type {
StoreMutatorIdentifier,
} from './vanilla.ts'
const { useDebugValue } = ReactExports
const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports
const { useDebugValue, useSyncExternalStore } = ReactExports
type ExtractState<S> = S extends { getState: () => infer T } ? T : never
@ -25,70 +20,32 @@ type ReadonlyStoreApi<T> = Pick<
'getState' | 'getInitialState' | 'subscribe'
>
type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
/** @deprecated please use api.getInitialState() */
getServerState?: () => ExtractState<S>
}
let didWarnAboutEqualityFn = false
const identity = <T>(arg: T): T => arg
export function useStore<S extends WithReact<ReadonlyStoreApi<unknown>>>(
export function useStore<S extends ReadonlyStoreApi<unknown>>(
api: S,
): ExtractState<S>
export function useStore<S extends WithReact<ReadonlyStoreApi<unknown>>, U>(
export function useStore<S extends ReadonlyStoreApi<unknown>, U>(
api: S,
selector: (state: ExtractState<S>) => U,
): U
/**
* @deprecated The usage with three arguments is deprecated. Use `useStoreWithEqualityFn` from 'zustand/traditional'. The usage with one or two arguments is not deprecated.
* https://github.com/pmndrs/zustand/discussions/1937
*/
export function useStore<S extends WithReact<ReadonlyStoreApi<unknown>>, U>(
api: S,
selector: (state: ExtractState<S>) => U,
equalityFn: ((a: U, b: U) => boolean) | undefined,
): U
export function useStore<TState, StateSlice>(
api: WithReact<ReadonlyStoreApi<TState>>,
api: ReadonlyStoreApi<TState>,
selector: (state: TState) => StateSlice = identity as any,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean,
) {
if (
import.meta.env?.MODE !== 'production' &&
equalityFn &&
!didWarnAboutEqualityFn
) {
console.warn(
"[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937",
)
didWarnAboutEqualityFn = true
}
const slice = useSyncExternalStoreWithSelector(
const slice = useSyncExternalStore(
api.subscribe,
api.getState,
api.getServerState || api.getInitialState,
selector,
equalityFn,
() => selector(api.getState()),
() => selector(api.getInitialState()),
)
useDebugValue(slice)
return slice
}
export type UseBoundStore<S extends WithReact<ReadonlyStoreApi<unknown>>> = {
export type UseBoundStore<S extends ReadonlyStoreApi<unknown>> = {
(): ExtractState<S>
<U>(selector: (state: ExtractState<S>) => U): U
/**
* @deprecated Use `createWithEqualityFn` from 'zustand/traditional'
*/
<U>(
selector: (state: ExtractState<S>) => U,
equalityFn: (a: U, b: U) => boolean,
): U
} & S
type Create = {
@ -98,26 +55,12 @@ type Create = {
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
) => UseBoundStore<Mutate<StoreApi<T>, Mos>>
/**
* @deprecated Use `useStore` hook to bind store
*/
<S extends StoreApi<unknown>>(store: S): UseBoundStore<S>
}
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
if (
import.meta.env?.MODE !== 'production' &&
typeof createState !== 'function'
) {
console.warn(
"[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.",
)
}
const api =
typeof createState === 'function' ? createStore(createState) : createState
const api = createStore(createState)
const useBoundStore: any = (selector?: any, equalityFn?: any) =>
useStore(api, selector, equalityFn)
const useBoundStore: any = (selector?: any) => useStore(api, selector)
Object.assign(useBoundStore, api)
@ -126,15 +69,3 @@ const createImpl = <T>(createState: StateCreator<T, [], []>) => {
export const create = (<T>(createState: StateCreator<T, [], []> | undefined) =>
createState ? createImpl(createState) : createImpl) as Create
/**
* @deprecated Use `import { create } from 'zustand'`
*/
export default ((createState: any) => {
if (import.meta.env?.MODE !== 'production') {
console.warn(
"[DEPRECATED] Default export is deprecated. Instead use `import { create } from 'zustand'`.",
)
}
return create(createState)
}) as Create

View File

@ -1,7 +1,6 @@
// import { useDebugValue } from 'react'
// import { useRef } from 'react'
// That doesnt work in ESM, because React libs are CJS only.
// The following is a workaround until ESM is supported.
// eslint-disable-next-line import/extensions
import ReactExports from 'react'
import { shallow } from '../vanilla/shallow.ts'
@ -14,6 +13,8 @@ export function useShallow<S, U>(selector: (state: S) => U): (state: S) => U {
const next = selector(state)
return shallow(prev.current, next)
? (prev.current as U)
: (prev.current = next)
: // It might not work with React Compiler
// eslint-disable-next-line react-compiler/react-compiler
(prev.current = next)
}
}

View File

@ -1,19 +1,2 @@
import { shallow } from './vanilla/shallow.ts'
// We will export this in v5 and remove default export
// export { shallow } from './vanilla/shallow.ts'
// export { useShallow } from './react/shallow.ts'
/**
* @deprecated Use `import { shallow } from 'zustand/shallow'`
*/
export default ((objA, objB) => {
if (import.meta.env?.MODE !== 'production') {
console.warn(
"[DEPRECATED] Default export is deprecated. Instead use `import { shallow } from 'zustand/shallow'`.",
)
}
return shallow(objA, objB)
}) as typeof shallow
export { shallow }
export { shallow } from './vanilla/shallow.ts'
export { useShallow } from './react/shallow.ts'

View File

@ -3,7 +3,6 @@
// Those don't work in ESM, because React libs are CJS only.
// See: https://github.com/pmndrs/valtio/issues/452
// The following is a workaround until ESM is supported.
// eslint-disable-next-line import/extensions
import ReactExports from 'react'
// eslint-disable-next-line import/extensions
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector'
@ -25,35 +24,27 @@ type ReadonlyStoreApi<T> = Pick<
'getState' | 'getInitialState' | 'subscribe'
>
type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
/** @deprecated please use api.getInitialState() */
getServerState?: () => ExtractState<S>
}
const identity = <T>(arg: T): T => arg
export function useStoreWithEqualityFn<
S extends WithReact<ReadonlyStoreApi<unknown>>,
>(api: S): ExtractState<S>
export function useStoreWithEqualityFn<S extends ReadonlyStoreApi<unknown>>(
api: S,
): ExtractState<S>
export function useStoreWithEqualityFn<
S extends WithReact<ReadonlyStoreApi<unknown>>,
U,
>(
export function useStoreWithEqualityFn<S extends ReadonlyStoreApi<unknown>, U>(
api: S,
selector: (state: ExtractState<S>) => U,
equalityFn?: (a: U, b: U) => boolean,
): U
export function useStoreWithEqualityFn<TState, StateSlice>(
api: WithReact<ReadonlyStoreApi<TState>>,
api: ReadonlyStoreApi<TState>,
selector: (state: TState) => StateSlice = identity as any,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean,
) {
const slice = useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getServerState || api.getInitialState,
api.getInitialState,
selector,
equalityFn,
)
@ -61,9 +52,7 @@ export function useStoreWithEqualityFn<TState, StateSlice>(
return slice
}
export type UseBoundStoreWithEqualityFn<
S extends WithReact<ReadonlyStoreApi<unknown>>,
> = {
export type UseBoundStoreWithEqualityFn<S extends ReadonlyStoreApi<unknown>> = {
(): ExtractState<S>
<U>(
selector: (state: ExtractState<S>) => U,

View File

@ -1,8 +1,9 @@
type SetStateInternal<T> = {
_(
partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],
replace?: boolean | undefined,
replace?: false,
): void
_(state: T | { _(state: T): T }['_'], replace: true): void
}['_']
export interface StoreApi<T> {
@ -10,10 +11,6 @@ export interface StoreApi<T> {
getState: () => T
getInitialState: () => T
subscribe: (listener: (state: T, prevState: T) => void) => () => void
/**
* @deprecated Use `unsubscribe` returned by `subscribe`
*/
destroy: () => void
}
type Get<T, K, F> = K extends keyof T ? T[K] : F
@ -92,94 +89,10 @@ const createStoreImpl: CreateStoreImpl = (createState) => {
return () => listeners.delete(listener)
}
const destroy: StoreApi<TState>['destroy'] = () => {
if (import.meta.env?.MODE !== 'production') {
console.warn(
'[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected.',
)
}
listeners.clear()
}
const api = { setState, getState, getInitialState, subscribe, destroy }
const api = { setState, getState, getInitialState, subscribe }
const initialState = (state = createState(setState, getState, api))
return api as any
}
export const createStore = ((createState) =>
createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore
/**
* @deprecated Use `import { createStore } from 'zustand/vanilla'`
*/
export default ((createState) => {
if (import.meta.env?.MODE !== 'production') {
console.warn(
"[DEPRECATED] Default export is deprecated. Instead use import { createStore } from 'zustand/vanilla'.",
)
}
return createStore(createState)
}) as CreateStore
// ---------------------------------------------------------
/**
* @deprecated Use `unknown` instead of `State`
*/
export type State = unknown
/**
* @deprecated Use `Partial<T> | ((s: T) => Partial<T>)` instead of `PartialState<T>`
*/
export type PartialState<T extends State> =
| Partial<T>
| ((state: T) => Partial<T>)
/**
* @deprecated Use `(s: T) => U` instead of `StateSelector<T, U>`
*/
export type StateSelector<T extends State, U> = (state: T) => U
/**
* @deprecated Use `(a: T, b: T) => boolean` instead of `EqualityChecker<T>`
*/
export type EqualityChecker<T> = (state: T, newState: T) => boolean
/**
* @deprecated Use `(state: T, previousState: T) => void` instead of `StateListener<T>`
*/
export type StateListener<T> = (state: T, previousState: T) => void
/**
* @deprecated Use `(slice: T, previousSlice: T) => void` instead of `StateSliceListener<T>`.
*/
export type StateSliceListener<T> = (slice: T, previousSlice: T) => void
/**
* @deprecated Use `(listener: (state: T) => void) => void` instead of `Subscribe<T>`.
*/
export type Subscribe<T extends State> = {
(listener: (state: T, previousState: T) => void): () => void
}
/**
* @deprecated You might be looking for `StateCreator`, if not then
* use `StoreApi<T>['setState']` instead of `SetState<T>`.
*/
export type SetState<T extends State> = {
_(
partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],
replace?: boolean | undefined,
): void
}['_']
/**
* @deprecated You might be looking for `StateCreator`, if not then
* use `StoreApi<T>['getState']` instead of `GetState<T>`.
*/
export type GetState<T extends State> = () => T
/**
* @deprecated Use `StoreApi<T>['destroy']` instead of `Destroy`.
*/
export type Destroy = () => void

View File

@ -1,4 +1,22 @@
export function shallow<T>(objA: T, objB: T) {
const isIterable = (obj: object): obj is Iterable<unknown> =>
Symbol.iterator in obj
const compareMapLike = (
iterableA: Iterable<[unknown, unknown]>,
iterableB: Iterable<[unknown, unknown]>,
) => {
const mapA = iterableA instanceof Map ? iterableA : new Map(iterableA)
const mapB = iterableB instanceof Map ? iterableB : new Map(iterableB)
if (mapA.size !== mapB.size) return false
for (const [key, value] of mapA) {
if (!Object.is(value, mapB.get(key))) {
return false
}
}
return true
}
export function shallow<T>(objA: T, objB: T): boolean {
if (Object.is(objA, objB)) {
return true
}
@ -11,26 +29,30 @@ export function shallow<T>(objA: T, objB: T) {
return false
}
if (objA instanceof Map && objB instanceof Map) {
if (objA.size !== objB.size) return false
for (const [key, value] of objA) {
if (!Object.is(value, objB.get(key))) {
if (isIterable(objA) && isIterable(objB)) {
const iteratorA = objA[Symbol.iterator]()
const iteratorB = objB[Symbol.iterator]()
let nextA = iteratorA.next()
let nextB = iteratorB.next()
if (
Array.isArray(nextA.value) &&
Array.isArray(nextB.value) &&
nextA.value.length === 2 &&
nextB.value.length === 2
) {
return compareMapLike(
objA as Iterable<[unknown, unknown]>,
objB as Iterable<[unknown, unknown]>,
)
}
while (!nextA.done && !nextB.done) {
if (!Object.is(nextA.value, nextB.value)) {
return false
}
nextA = iteratorA.next()
nextB = iteratorB.next()
}
return true
}
if (objA instanceof Set && objB instanceof Set) {
if (objA.size !== objB.size) return false
for (const value of objA) {
if (!objB.has(value)) {
return false
}
}
return true
return !!nextA.done && !!nextB.done
}
const keysA = Object.keys(objA)
@ -39,7 +61,7 @@ export function shallow<T>(objA: T, objB: T) {
}
for (const keyA of keysA) {
if (
!Object.prototype.hasOwnProperty.call(objB, keyA as string) ||
!Object.hasOwn(objB, keyA as string) ||
!Object.is(objA[keyA as keyof T], objB[keyA as keyof T])
) {
return false

View File

@ -30,7 +30,6 @@ it('creates a store hook and api object', () => {
[Function],
[Function],
{
"destroy": [Function],
"getInitialState": [Function],
"getState": [Function],
"setState": [Function],
@ -473,23 +472,47 @@ it('can set the store without merging', () => {
expect(getState()).toEqual({ b: 2 })
})
it('can destroy the store', () => {
const { destroy, getState, setState, subscribe } = create(() => ({
value: 1,
}))
it('only calls selectors when necessary with static selector', async () => {
type State = { a: number; b: number }
const useBoundStore = createWithEqualityFn<State>(() => ({ a: 0, b: 0 }))
const { setState } = useBoundStore
let staticSelectorCallCount = 0
subscribe(() => {
throw new Error('did not clear listener on destroy')
})
destroy()
function staticSelector(s: State) {
staticSelectorCallCount++
return s.a
}
setState({ value: 2 })
expect(getState().value).toEqual(2)
function Component() {
useBoundStore(staticSelector)
return (
<>
<div>static: {staticSelectorCallCount}</div>
</>
)
}
const { rerender, findByText } = render(
<>
<Component />
</>,
)
await findByText('static: 1')
rerender(
<>
<Component />
</>,
)
await findByText('static: 1')
act(() => setState({ a: 1, b: 1 }))
await findByText('static: 2')
})
it('only calls selectors when necessary', async () => {
it('only calls selectors when necessary (traditional)', async () => {
type State = { a: number; b: number }
const useBoundStore = create<State>(() => ({ a: 0, b: 0 }))
const useBoundStore = createWithEqualityFn<State>(() => ({ a: 0, b: 0 }))
const { setState } = useBoundStore
let inlineSelectorCallCount = 0
let staticSelectorCallCount = 0
@ -695,3 +718,20 @@ it('works with non-object state', async () => {
fireEvent.click(getByText('button'))
await findByText('count: 2')
})
it('works with "undefined" state', async () => {
const useUndefined = create(() => undefined)
const Component = () => {
const str = useUndefined((v) => v || 'undefined')
return <div>str: {str}</div>
}
const { findByText } = render(
<StrictMode>
<Component />
</StrictMode>,
)
await findByText('str: undefined')
})

View File

@ -1,174 +0,0 @@
import {
Component as ClassComponent,
StrictMode,
useCallback,
useEffect,
useState,
} from 'react'
import type { ReactNode } from 'react'
import { render } from '@testing-library/react'
import { afterEach, it, vi } from 'vitest'
import { create } from 'zustand'
import type { StoreApi } from 'zustand'
import createContext from 'zustand/context'
import { subscribeWithSelector } from 'zustand/middleware'
const consoleError = console.error
afterEach(() => {
console.error = consoleError
})
type CounterState = {
count: number
inc: () => void
}
it('creates and uses context store', async () => {
const { Provider, useStore } = createContext<StoreApi<CounterState>>()
const createStore = () =>
create<CounterState>((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
function Counter() {
const { count, inc } = useStore()
useEffect(inc, [inc])
return <div>count: {count * 1}</div>
}
const { findByText } = render(
<>
<Provider createStore={createStore}>
<Counter />
</Provider>
</>,
)
await findByText('count: 1')
})
it('uses context store with selectors', async () => {
const { Provider, useStore } = createContext<StoreApi<CounterState>>()
const createStore = () =>
create<CounterState>((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
function Counter() {
const count = useStore((state) => state.count)
const inc = useStore((state) => state.inc)
useEffect(inc, [inc])
return <div>count: {count * 1}</div>
}
const { findByText } = render(
<>
<Provider createStore={createStore}>
<Counter />
</Provider>
</>,
)
await findByText('count: 1')
})
it('uses context store api', async () => {
const createStore = () =>
create<CounterState>()(
subscribeWithSelector((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
})),
)
type CustomStore = ReturnType<typeof createStore>
const { Provider, useStoreApi } = createContext<CustomStore>()
function Counter() {
const storeApi = useStoreApi()
const [count, setCount] = useState(0)
useEffect(
() =>
storeApi.subscribe(
(state) => state.count,
() => setCount(storeApi.getState().count),
),
[storeApi],
)
useEffect(() => {
storeApi.setState({ count: storeApi.getState().count + 1 })
}, [storeApi])
useEffect(() => {
if (count === 1) {
storeApi.destroy()
storeApi.setState({ count: storeApi.getState().count + 1 })
}
}, [storeApi, count])
return <div>count: {count * 1}</div>
}
const { findByText } = render(
<>
<Provider createStore={createStore}>
<Counter />
</Provider>
</>,
)
await findByText('count: 1')
})
it('throws error when not using provider', async () => {
console.error = vi.fn()
class ErrorBoundary extends ClassComponent<
{ children?: ReactNode | undefined },
{ hasError: boolean }
> {
constructor(props: { children?: ReactNode | undefined }) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError() {
return { hasError: true }
}
render() {
return this.state.hasError ? <div>errored</div> : this.props.children
}
}
const { useStore } = createContext<StoreApi<CounterState>>()
function Component() {
useStore()
return <div>no error</div>
}
const { findByText } = render(
<StrictMode>
<ErrorBoundary>
<Component />
</ErrorBoundary>
</StrictMode>,
)
await findByText('errored')
})
it('useCallback with useStore infers types correctly', async () => {
const { useStore } = createContext<StoreApi<CounterState>>()
function _Counter() {
const _x = useStore(useCallback((state) => state.count, []))
expectAreTypesEqual<typeof _x, number>().toBe(true)
}
})
const expectAreTypesEqual = <A, B>() => ({
toBe: (
_: (<T>() => T extends B ? 1 : 0) extends <T>() => T extends A ? 1 : 0
? true
: false,
) => {},
})

View File

@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { Mock } from 'vitest'
import { devtools, redux } from 'zustand/middleware'
import { createStore } from 'zustand/vanilla'
import type { StoreApi } from 'zustand/vanilla'
@ -17,12 +18,12 @@ type TupleOfEqualLength<Arr extends unknown[], T> = number extends Arr['length']
type Connection = {
subscribers: ((message: unknown) => void)[]
api: {
subscribe: any
unsubscribe: any
send: any
init: any
error: any
dispatch?: any
subscribe: Mock<any>
unsubscribe: Mock<any>
send: Mock<any>
init: Mock<any>
error: Mock<any>
dispatch?: Mock<any>
}
}
const namedConnections = new Map<string | undefined, Connection>()
@ -150,25 +151,10 @@ describe('If there is no extension installed...', () => {
}).not.toThrow()
})
it('does not warn if not enabled', async () => {
it('does not warn', async () => {
createStore(devtools(() => ({ count: 0 })))
expect(console.warn).not.toBeCalled()
})
it('[DEV-ONLY] warns if enabled in dev mode', async () => {
createStore(devtools(() => ({ count: 0 }), { enabled: true }))
expect(console.warn).toBeCalled()
})
it.skip('[PRD-ONLY] does not warn if not in dev env', async () => {
createStore(devtools(() => ({ count: 0 })))
expect(console.warn).not.toBeCalled()
})
it.skip('[PRD-ONLY] does not warn if not in dev env even if enabled', async () => {
createStore(devtools(() => ({ count: 0 }), { enabled: true }))
expect(console.warn).not.toBeCalled()
})
})
describe('When state changes...', () => {
@ -690,7 +676,7 @@ it('preserves isRecording after setting from devtools', async () => {
/* features:
* [] if name is undefined - use multiple devtools connections.
* [] if name and store is defined - use connection for specific 'name'.
* [] if two stores are coonected to one 'name' group and.
* [] if two stores are connected to one 'name' group and.
* another connected to another 'name' group, then feature should work
* [] check actions with this feature, for multiple stores that store prefixes are added -
* [] - reset
@ -732,9 +718,7 @@ describe('when redux connection was called on multiple stores with `name` undefi
})
describe('when `store` property was provided in `devtools` call in options', () => {
// FIXME: Run this test separately in CI, until we're able to test modules in isolation i.e. use jest.resetModule and re-import modules in each test
// Relevant issues https://github.com/nodejs/node/issues/35889
it('[CI-MATRIX-1] should create single connection for all indernal calls of .connect and `store` is not passed to .connect', async () => {
it('should create single connection for all internal calls of .connect and `store` is not passed to .connect', async () => {
const { devtools: newDevtools } = await import('zustand/middleware')
const options1 = { store: 'store1123', foo: 'bar1' }
@ -755,9 +739,7 @@ describe('when redux connection was called on multiple stores with `name` undefi
})
})
// FIXME: Run this test separately in CI, until we're able to test modules in isolation i.e. use jest.resetModule and re-import modules in each test
// Relevant issues https://github.com/nodejs/node/issues/35889
it('[CI-MATRIX-2] should call `.init` on single connection with combined states after each `create(devtools` call', async () => {
it('should call `.init` on single connection with combined states after each `create(devtools` call', async () => {
const { devtools: newDevtools } = await import('zustand/middleware')
const options1 = { store: 'store12' }
@ -851,9 +833,7 @@ describe('when redux connection was called on multiple stores with `name` provid
})
})
// FIXME: Run this test separately in CI, until we're able to test modules in isolation i.e. use jest.resetModule and re-import modules in each test
// Relevant issues https://github.com/nodejs/node/issues/35889
it('[CI-MATRIX-3] should call `.init` on single connection with combined states after each `create(devtools` call', async () => {
it('should call `.init` on single connection with combined states after each `create(devtools` call', async () => {
const { devtools: newDevtools } = await import('zustand/middleware')
const connectionNameGroup1 = 'test1'
const connectionNameGroup2 = 'test2'
@ -2364,9 +2344,7 @@ describe('when create devtools was called multiple times with `name` and `store`
console.error = originalConsoleError
})
// FIXME: Run this test separately in CI, until we're able to test modules in isolation i.e. use jest.resetModule and re-import modules in each test
// Relevant issues https://github.com/nodejs/node/issues/35889
it('[CI-MATRIX-4] does nothing even if there is `api.dispatch`, connections isolated from each other', async () => {
it('does nothing even if there is `api.dispatch`, connections isolated from each other', async () => {
const { devtools: newDevtools } = await import('zustand/middleware')
const name1 = 'name1'
@ -2414,9 +2392,7 @@ describe('when create devtools was called multiple times with `name` and `store`
expect((api2 as any).dispatch).not.toBeCalled()
})
// FIXME: Run this test separately in CI, until we're able to test modules in isolation i.e. use jest.resetModule and re-import modules in each test
// Relevant issues https://github.com/nodejs/node/issues/35889
it('[CI-MATRIX-5] dispatches with `api.dispatch` when `api.dispatchFromDevtools` is set to true, connections are isolated from each other', async () => {
it('dispatches with `api.dispatch` when `api.dispatchFromDevtools` is set to true, connections are isolated from each other', async () => {
const { devtools: newDevtools } = await import('zustand/middleware')
const name1 = 'name1'
const name2 = 'name2'

View File

@ -19,11 +19,9 @@ const useBearStore = create<BearStoreState & BearStoreAction>((set) => ({
}))
function Counter() {
const { bears, increasePopulation } = useBearStore(
({ bears, increasePopulation }) => ({
bears,
increasePopulation,
}),
const bears = useBearStore(({ bears }) => bears)
const increasePopulation = useBearStore(
({ increasePopulation }) => increasePopulation,
)
useEffect(() => {
@ -37,7 +35,10 @@ describe.skipIf(!React.version.startsWith('18'))(
'ssr behavior with react 18',
() => {
it('should handle different states between server and client correctly', async () => {
const { hydrateRoot }: any = await vi.importActual('react-dom/client')
const { hydrateRoot } =
await vi.importActual<typeof import('react-dom/client')>(
'react-dom/client',
)
const markup = renderToString(
<React.Suspense fallback={<div>Loading...</div>}>
@ -69,7 +70,10 @@ describe.skipIf(!React.version.startsWith('18'))(
bears: 0,
}))
const { hydrateRoot }: any = await vi.importActual('react-dom/client')
const { hydrateRoot } =
await vi.importActual<typeof import('react-dom/client')>(
'react-dom/client',
)
const Component = () => {
const bears = useStore((state) => state.bears)

View File

@ -79,7 +79,6 @@ it('can use exposed types', () => {
_stateSelector: (state: ExampleState) => number,
_storeApi: StoreApi<ExampleState>,
_subscribe: StoreApi<ExampleState>['subscribe'],
_destroy: StoreApi<ExampleState>['destroy'],
_equalityFn: (a: ExampleState, b: ExampleState) => boolean,
_stateCreator: StateCreator<ExampleState>,
_useBoundStore: UseBoundStore<StoreApi<ExampleState>>,
@ -96,7 +95,6 @@ it('can use exposed types', () => {
selector,
storeApi,
storeApi.subscribe,
storeApi.destroy,
equalityFn,
stateCreator,
useBoundStore,
@ -114,9 +112,9 @@ it('should have correct (partial) types for setState', () => {
const store = create<Count>((set) => ({
count: 0,
// @ts-expect-error we shouldn't be able to set count to undefined [LATEST-TS-ONLY]
// @ts-expect-error we shouldn't be able to set count to undefined
a: () => set(() => ({ count: undefined })),
// @ts-expect-error we shouldn't be able to set count to undefined [LATEST-TS-ONLY]
// @ts-expect-error we shouldn't be able to set count to undefined
b: () => set({ count: undefined }),
c: () => set({ count: 1 }),
}))
@ -132,9 +130,9 @@ it('should have correct (partial) types for setState', () => {
store.setState({})
store.setState((previous) => previous)
// @ts-expect-error type undefined is not assignable to type number [LATEST-TS-ONLY]
// @ts-expect-error type undefined is not assignable to type number
store.setState({ count: undefined })
// @ts-expect-error type undefined is not assignable to type number [LATEST-TS-ONLY]
// @ts-expect-error type undefined is not assignable to type number
store.setState((state) => ({ ...state, count: undefined }))
})

View File

@ -22,7 +22,6 @@ it('create a store', () => {
[Function],
[Function],
{
"destroy": [Function],
"getInitialState": [Function],
"getState": [Function],
"setState": [Function],
@ -30,7 +29,6 @@ it('create a store', () => {
},
],
"result": {
"destroy": [Function],
"getInitialState": [Function],
"getState": [Function],
"setState": [Function],
@ -140,17 +138,3 @@ it('works with non-object state', () => {
expect(store.getState()).toBe(2)
})
it('can destroy the store', () => {
const { destroy, getState, setState, subscribe } = createStore(() => ({
value: 1,
}))
subscribe(() => {
throw new Error('did not clear listener on destroy')
})
destroy()
setState({ value: 2 })
expect(getState().value).toEqual(2)
})

View File

@ -91,6 +91,12 @@ describe('shallow', () => {
expect(shallow(firstFnCompare, secondFnCompare)).toBe(false)
})
it('compares URLSearchParams', () => {
const a = new URLSearchParams({ hello: 'world' })
const b = new URLSearchParams({ zustand: 'shallow' })
expect(shallow(a, b)).toBe(false)
})
})
describe('unsupported cases', () => {