mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
v5 (#2138)
* 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:
parent
53e5a2a7f5
commit
e247220ece
@ -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",
|
||||
|
||||
21
.github/workflows/test-multiple-builds.yml
vendored
21
.github/workflows/test-multiple-builds.yml
vendored
@ -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
|
||||
|
||||
30
.github/workflows/test-multiple-versions.yml
vendored
30
.github/workflows/test-multiple-versions.yml
vendored
@ -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 }}
|
||||
|
||||
11
.github/workflows/test-old-typescript.yml
vendored
11
.github/workflows/test-old-typescript.yml
vendored
@ -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
|
||||
|
||||
@ -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 }],
|
||||
],
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
171
docs/migrations/migrating-to-v5.md
Normal file
171
docs/migrations/migrating-to-v5.md
Normal 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
|
||||
174
package.json
174
package.json
@ -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
1358
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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'
|
||||
|
||||
|
||||
119
rollup.config.js
119
rollup.config.js
@ -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'),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
102
src/context.ts
102
src/context.ts
@ -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
|
||||
@ -1,3 +1,2 @@
|
||||
export * from './vanilla.ts'
|
||||
export * from './react.ts'
|
||||
export { default } from './react.ts'
|
||||
|
||||
@ -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" } } }
|
||||
`,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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][] = [],
|
||||
|
||||
93
src/react.ts
93
src/react.ts
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
})
|
||||
|
||||
@ -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,
|
||||
) => {},
|
||||
})
|
||||
@ -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'
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 }))
|
||||
})
|
||||
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user