Compare commits

...

24 Commits

Author SHA1 Message Date
Danilo Britto
95850110ab
docs: update map and set guide (#3258)
* Enhance Maps and Sets usage documentation in Zustand

Expanded the documentation on using Maps and Sets in Zustand, including guidelines for reading and updating these data structures. Added examples for creating new instances and explained the importance of reference changes for state updates.

* feat: update format
2025-10-12 07:59:26 -05:00
Ehsan Aslani
20ad3f8891
Add multiplayer to third-party libraries (#3241)
* docs: Add multiplayer  middleware to third-party libraries

* Update docs/integrations/third-party-libraries.md

---------

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
2025-09-30 10:26:09 +09:00
Ushran Gouhar
56a39b6a0f
Update hook name from useStore to useBear (#3233)
Zustand already provides a `useStore` hook, and reusing the same name for a custom hook could lead to confusion if someone accidentally imports the wrong one. To avoid this, I’ve renamed the custom hook to `useBear`. The name is short, memorable, and aligns with Zustand’s playful style.
2025-09-12 17:30:50 +09:00
Wonsuk Choi
612d5c4647
chore(eslint.config): replace deprecated 'tseslint.config' with 'defineConfig' (#3231)
Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
2025-09-10 19:37:07 +09:00
Wonsuk Choi
fd1ea8ca9d
ci(.github/workflows): update 'actions/checkout' and 'actions/setup-node' to v5 (#3227)
* ci(.github/workflows): update 'actions/checkout' and 'actions/setup-node' to v5

* empty commit

* empty commit

* chore: empty commit

* chore: empty commit

* empty commit

---------

Co-authored-by: daishi <daishi@axlight.com>
2025-09-10 17:47:33 +09:00
Jinsoo Lee
4db616e1f7
chore: upgrade pnpm to v10.15.0 (#3223) 2025-08-29 10:09:33 +09:00
Dan Tonon
85e3f2929a
Add new Zustand third-party links for zustand-create-setter-fn and zustand-utils (#3222)
* Add new Zustand third-party links for `zustand-create-setter-fn` and `zustand-utils`

* Apply suggestions from code review

---------

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
2025-08-28 11:18:37 +09:00
daishi
0ea8be27db 5.0.8 2025-08-20 08:16:10 +09:00
Daishi Kato
cadcd3eb91
chore: update dev dependencies (#3218) 2025-08-20 08:15:23 +09:00
Daishi Kato
feddc0c210
feat(middleare/persist): return storage promise from setState (#3206)
* feat(middleare/persist): return storage promise from setState

* refactor types (technically breaking)

* another breaking change in types

* make public types not breaking
2025-08-20 08:08:35 +09:00
Daishi Kato
2cc19881fa
fix(shallow): undefined value (#3205)
* add failing test

* check with .has
2025-08-20 07:52:06 +09:00
Daishi Kato
c4085a4ff0
chore: strict unused check (#3215) 2025-08-19 09:26:59 +09:00
SungHyun Kim
f831bc8d71
fix broken links in the toc of create-store.md (#3207) 2025-08-09 10:04:58 -05:00
daishi
463c9e3ea6 5.0.7 2025-07-31 09:12:34 +09:00
Daishi Kato
30e36798a4
chore: udpate dev dependencies (#3194) 2025-07-31 09:11:53 +09:00
Daishi Kato
5aa923e8c3
fix(react): useCallback for getSnapshot (#3192) 2025-07-31 09:06:30 +09:00
Danilo Britto
41fed0fe6f
docs: update how to reset state guide (#3187) 2025-07-20 09:26:07 -05:00
Danilo Britto
aba78319bb
Update use-store-with-equality-fn.md (#3185) 2025-07-19 06:45:34 -05:00
Malte Hecht
2879505e39
fix: broken link to ssr and hydration (#3183) 2025-07-16 09:18:27 -05:00
Danilo Britto
1b7eb6907c
docs: update badge (#3181) 2025-07-14 11:11:46 -05:00
Danilo Britto
84b112e4a2
docs: update redux api docs (#3180)
* Update redux.md

* Update redux.md
2025-07-14 10:39:56 -05:00
Danilo Britto
4ad3977b38
Update redux.md (#3176) 2025-07-13 02:32:01 -05:00
soyboi
56909808fa
docs: add currying in for createStore (#3171) 2025-07-09 08:51:58 -05:00
Henry8192
149c286342
remove redundant dash in persist parameters (#3170) 2025-07-08 08:07:57 +09:00
26 changed files with 1148 additions and 974 deletions

View File

@ -6,9 +6,9 @@ jobs:
compressed_size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 'lts/*'
cache: 'pnpm'

View File

@ -6,9 +6,9 @@ jobs:
preview_release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 'lts/*'
cache: 'pnpm'

View File

@ -8,9 +8,9 @@ jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org'

View File

@ -15,9 +15,9 @@ jobs:
build: [cjs, esm]
env: [development] # [development, production]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 'lts/*'
cache: 'pnpm'

View File

@ -19,12 +19,12 @@ jobs:
- 18.3.1
- 19.0.0
- 19.1.0
- 19.2.0-canary-06e89951-20250620
- 0.0.0-experimental-06e89951-20250620
- 19.2.0-canary-0bdb9206-20250818
- 0.0.0-experimental-0bdb9206-20250818
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 'lts/*'
cache: 'pnpm'

View File

@ -13,6 +13,7 @@ jobs:
fail-fast: false
matrix:
typescript:
- 5.9.2
- 5.8.3
- 5.7.3
- 5.6.3
@ -28,9 +29,9 @@ jobs:
- 4.6.4
- 4.5.5
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 'lts/*'
cache: 'pnpm'

View File

@ -10,9 +10,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 'lts/*'
cache: 'pnpm'

View File

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

View File

@ -11,7 +11,7 @@ const someStore = createStore(stateCreatorFn)
```
- [Types](#types)
- [Signature](#createstore-signature)
- [Signature](#signature)
- [Reference](#reference)
- [Usage](#usage)
- [Updating state based on previous state](#updating-state-based-on-previous-state)
@ -20,7 +20,7 @@ const someStore = createStore(stateCreatorFn)
- [Updating Arrays in State](#updating-arrays-in-state)
- [Subscribing to state updates](#subscribing-to-state-updates)
- [Troubleshooting](#troubleshooting)
- [Ive updated the state, but the screen doesnt update](#ive-updated-the-state-but-the-screen-doesnt-update)
- [Ive updated the state, but the screen doesnt update](#ive-updated-the-state,-but-the-screen-doesnt-update)
## Types

View File

@ -45,7 +45,7 @@ The `set` function _merges_ state.
```js
import { create } from 'zustand'
const useStore = create((set) => ({
const useBear = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
@ -61,12 +61,12 @@ will re-render when that state changes.
```jsx
function BearCounter() {
const bears = useStore((state) => state.bears)
const bears = useBear((state) => state.bears)
return <h1>{bears} bears around here...</h1>
}
function Controls() {
const increasePopulation = useStore((state) => state.increasePopulation)
const increasePopulation = useBear((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
```

View File

@ -99,7 +99,7 @@ interface BearState {
increment: () => void
}
const store = createStore<BearState>((set) => ({
const store = createStore<BearState>()((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
increment: () => set((state) => ({ bears: state.bears + 1 })),

View File

@ -6,37 +6,10 @@ nav: 12
The following pattern can be used to reset the state to its initial value.
```ts
import { create } from 'zustand'
// define types for state values and actions separately
type State = {
salmon: number
tuna: number
}
type Actions = {
addSalmon: (qty: number) => void
addTuna: (qty: number) => void
reset: () => void
}
// define the initial state
const initialState: State = {
salmon: 0,
tuna: 0,
}
// create store
const useSlice = create<State & Actions>()((set, get) => ({
...initialState,
addSalmon: (qty: number) => {
set({ salmon: get().salmon + qty })
},
addTuna: (qty: number) => {
set({ tuna: get().tuna + qty })
},
const useSomeStore = create<State & Actions>()((set, get, store) => ({
// your code here
reset: () => {
set(initialState)
set(store.getInitialState())
},
}))
```
@ -58,9 +31,8 @@ const resetAllStores = () => {
export const create = (<T>() => {
return (stateCreator: StateCreator<T>) => {
const store = actualCreate(stateCreator)
const initialState = store.getInitialState()
storeResetFns.add(() => {
store.setState(initialState, true)
store.setState(store.getInitialState(), true)
})
return store
}
@ -69,6 +41,5 @@ export const create = (<T>() => {
## CodeSandbox Demo
- Basic: https://codesandbox.io/s/zustand-how-to-reset-state-basic-demo-rrqyon
- Advanced: https://codesandbox.io/s/zustand-how-to-reset-state-advanced-demo-gtu0qe
- Immer: https://codesandbox.io/s/how-to-reset-state-advance-immer-demo-nyet3f
- Basic: https://stackblitz.com/edit/zustand-how-to-reset-state-basic
- Advanced: https://stackblitz.com/edit/zustand-how-to-reset-state-advanced

View File

@ -3,25 +3,113 @@ title: Map and Set Usage
nav: 10
---
You need to wrap Maps and Sets inside an object. When you want its update to be reflected (e.g. in React),
you do it by calling `setState` on it:
# Map and Set in Zustand
**You can view a codesandbox here: https://codesandbox.io/s/late-https-bxz9qy**
Map and Set are mutable data structures. To use them in Zustand, you must create new instances when updating.
```js
import { create } from 'zustand'
## Map
const useFooBar = create(() => ({ foo: new Map(), bar: new Set() }))
### Reading a Map
function doSomething() {
// doing something...
```typescript
const foo = useSomeStore((state) => state.foo)
```
// If you want to update some React component that uses `useFooBar`, you have to call setState
// to let React know that an update happened.
// Following React's best practices, you should create a new Map/Set when updating them:
useFooBar.setState((prev) => ({
foo: new Map(prev.foo).set('newKey', 'newValue'),
bar: new Set(prev.bar).add('newKey'),
}))
### Updating a Map
Always create a new Map instance:
```ts
// Update single entry
set((state) => ({
foo: new Map(state.foo).set(key, value),
}))
// Delete entry
set((state) => {
const next = new Map(state.foo)
next.delete(key)
return { foo: next }
})
// Update multiple entries
set((state) => {
const next = new Map(state.foo)
next.set('key1', 'value1')
next.set('key2', 'value2')
return { foo: next }
})
// Clear
set({ foo: new Map() })
```
## Set
### Reading a Set
```ts
const bar = useSomeStore((state) => state.bar)
```
### Updating a Set
Always create a new Set instance:
```ts
// Add item
set((state) => ({
bar: new Set(state.bar).add(item),
}))
// Delete item
set((state) => {
const next = new Set(state.bar)
next.delete(item)
return { bar: next }
})
// Toggle item
set((state) => {
const next = new Set(state.bar)
next.has(item) ? next.delete(item) : next.add(item)
return { bar: next }
})
// Clear
set({ bar: new Set() })
```
## Why New Instances?
Zustand detects changes by comparing references. Mutating a Map or Set doesn't change its reference:
```ts
// ❌ Wrong - same reference, no re-render
set((state) => {
state.foo.set(key, value)
return { foo: state.foo }
})
// ✅ Correct - new reference, triggers re-render
set((state) => ({
foo: new Map(state.foo).set(key, value),
}))
```
## Pitfall: Type Hints for Empty Collections
Provide type hints when initializing empty Maps and Sets:
```ts
{
ids: new Set([] as string[]),
users: new Map([] as [string, User][])
}
```
Without type hints, TypeScript infers `never[]` which prevents adding items later.
## CodeSandbox Demo
Basic: https://stackblitz.com/edit/vitejs-vite-5cu5ddvx

View File

@ -15,7 +15,7 @@ These challenges include:
and again on the client. Having different outputs on both the client and the server will result
in "hydration errors." The store will have to be initialized on the server and then
re-initialized on the client with the same data in order to avoid that. Please read more about
that in our [SSR and Hydration](./ssr-and-hydration) guide.
that in our [SSR and Hydration](./ssr-and-hydration.md) guide.
- **SPA routing friendly:** Next.js supports a hybrid model for client side routing, which means
that in order to reset a store, we need to initialize it at the component level using a
`Context`.

View File

@ -20,7 +20,7 @@ const someState = useStoreWithEqualityFn(store, selectorFn, equalityFn)
- [Signature](#signature)
- [Reference](#reference)
- [Usage](#usage)
- [Use a vanilla store in React](#use-a-vanilla-store-in-react)
- [Using a global vanilla store in React](#using-a-global-vanilla-store-in-react)
- [Using dynamic vanilla stores in React](#using-dynamic-global-vanilla-stores-in-react)
- [Using scoped (non-global) vanilla store in React](#using-scoped-non-global-vanilla-store-in-react)
- [Using dynamic scoped (non-global) vanilla stores in React](#using-dynamic-scoped-non-global-vanilla-stores-in-react)

View File

@ -18,6 +18,7 @@ This can be done using third-party libraries created by the community.
- [@csark0812/zustand-expo-devtools](https://github.com/csark0812/zustand-expo-devtools) — 🧭 Connect Zustand to Redux DevTools in Expo + React Native using the official Expo DevTools plugin system.
- [@davstack/store](https://www.npmjs.com/package/@davstack/store) — A zustand store factory that auto generates selectors with get/set/use methods, supports inferred types, and makes global / local state management easy.
- [@dhmk/zustand-lens](https://github.com/dhmk083/dhmk-zustand-lens) — Lens support for Zustand.
- [@hpkv/zustand-multiplayer](https://github.com/hpkv-io/zustand-multiplayer/tree/main/packages/zustand-multiplayer) — HPKV multiplayer middleware for building realtime collaborative applications
- [@liveblocks/zustand](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks-zustand) — Liveblocks middleware to make your application multiplayer.
- [@prncss-xyz/zustand-optics](https://github.com/prncss-xyz/zustand-optics) — An adapter for [optics-ts](https://github.com/akheron/optics-ts).
- [auto-zustand-selectors-hook](https://github.com/Albert-Gao/auto-zustand-selectors-hook) — Automatic generation of Zustand hooks with Typescript support.
@ -47,6 +48,7 @@ This can be done using third-party libraries created by the community.
- [zustand-computed-state](https://github.com/yasintz/zustand-computed-state) — Simple middleware to add computed states.
- [zustand-constate](https://github.com/ntvinhit/zustand-constate) — Context-based state management based on Zustand and taking ideas from Constate.
- [zustand-context](https://github.com/fredericoo/zustand-context) — Create a zustand store in React Context, containing an initial value, or use it in your components with isolated, mockable instances.
- [zustand-create-setter-fn](https://www.npmjs.com/package/zustand-create-setter-fn) — A fully type safe utility for Zustand that allows you to easily update state using React style `setState` functions (framework agnostic, doesn't require React).
- [zustand-di](https://github.com/charkour/zustand-di) — use react props to init zustand stores
- [zustand-forms](https://github.com/Conduct/zustand-forms) — Fast, type safe form states as Zustand stores.
- [zustand-hash-storage](https://github.com/MartinGamesCZ/zustand-hash-storage) — Zustand middleware for saving state into URL hash, b64 encoded (can be configured) and debounce timer.
@ -66,6 +68,7 @@ This can be done using third-party libraries created by the community.
- [zustand-slices](https://github.com/zustandjs/zustand-slices) — A slice utility for Zustand.
- [zustand-store-addons](https://github.com/Diablow/zustand-store-addons) — React state management addons for Zustand.
- [zustand-sync-tabs](https://github.com/mayank1513/zustand-sync-tabs) — Zustand middleware to easily sync Zustand state between tabs/windows/iframes with same origin.
- [zustand-utils](https://www.npmjs.com/package/zustand-utils) — Utilities for Zustand - a `createContext` replacement, a devtools wrapper, and a store-updater factory function.
- [zustand-valtio](https://github.com/zustandjs/zustand-valtio) — A sweet combination of Zustand and Valtio
- [zustand-vue](https://github.com/AwesomeDevin/zustand-vue) — State management for vue (Vue3 / Vue2) based on zustand.
- [zustand-x](https://github.com/udecode/zustand-x) — Zustand store factory for a best-in-class developer experience.

View File

@ -52,7 +52,7 @@ persist<T, U>(stateCreatorFn: StateCreator<T, [], []>, persistOptions?: PersistO
Usually, you will return an object with the methods you want to expose.
- `persistOptions`: An object to define storage options.
- `name`: A unique name of the item for your store in the storage.
- **optional** `storage`: Defaults to `createJSONStorage(() => localStorage)`. -
- **optional** `storage`: Defaults to `createJSONStorage(() => localStorage)`.
- **optional** `partialize`: A function to filter state fields before persisting it.
- **optional** `onRehydrateStorage`: A function or function returning a function that allows
custom logic before and after state rehydration.

View File

@ -70,7 +70,9 @@ type PersonStoreAction =
| { type: 'person/setLastName'; lastName: string }
| { type: 'person/setEmail'; email: string }
type PersonStore = PersonStoreState & PersonStoreActions
type PersonStore = PersonStoreState & {
dispatch: (action: PersonStoreAction) => PersonStoreAction
}
const personStoreReducer = (
state: PersonStoreState,

View File

@ -1,5 +1,6 @@
import eslint from '@eslint/js'
import vitest from '@vitest/eslint-plugin'
import { defineConfig } from 'eslint/config'
import importPlugin from 'eslint-plugin-import'
import jestDom from 'eslint-plugin-jest-dom'
import react from 'eslint-plugin-react'
@ -7,7 +8,7 @@ import reactHooks from 'eslint-plugin-react-hooks'
import testingLibrary from 'eslint-plugin-testing-library'
import tseslint from 'typescript-eslint'
export default tseslint.config(
export default defineConfig(
{
ignores: ['dist/', 'examples/'],
},
@ -67,7 +68,7 @@ export default tseslint.config(
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'react-hooks/react-compiler': 'error',
@ -80,7 +81,6 @@ export default tseslint.config(
...vitest.configs.recommended,
rules: {
'import/extensions': ['error', 'never'],
'@typescript-eslint/no-unused-vars': 'off',
'testing-library/no-node-access': 'off',
'vitest/consistent-test-it': [
'error',

View File

@ -3,7 +3,7 @@
"description": "🐻 Bear necessities for state management in React",
"private": true,
"type": "commonjs",
"version": "5.0.6",
"version": "5.0.8",
"main": "./index.js",
"types": "./index.d.ts",
"typesVersions": {
@ -114,45 +114,45 @@
"url": "https://github.com/pmndrs/zustand/issues"
},
"homepage": "https://github.com/pmndrs/zustand",
"packageManager": "pnpm@9.15.5",
"packageManager": "pnpm@10.15.0",
"devDependencies": {
"@eslint/js": "^9.29.0",
"@eslint/js": "^9.33.0",
"@redux-devtools/extension": "^3.3.0",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-replace": "^6.0.2",
"@rollup/plugin-typescript": "12.1.2",
"@testing-library/jest-dom": "^6.6.3",
"@rollup/plugin-typescript": "12.1.4",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/react": "^16.3.0",
"@types/node": "^24.0.3",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/node": "^24.3.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@types/use-sync-external-store": "^1.5.0",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/eslint-plugin": "^1.2.7",
"@vitest/eslint-plugin": "^1.3.4",
"@vitest/ui": "^3.2.4",
"esbuild": "^0.25.5",
"eslint": "9.29.0",
"eslint-import-resolver-typescript": "^4.4.3",
"esbuild": "^0.25.9",
"eslint": "9.33.0",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "6.0.0-rc.1",
"eslint-plugin-testing-library": "^7.5.3",
"eslint-plugin-testing-library": "^7.6.6",
"immer": "^10.1.1",
"jsdom": "^26.1.0",
"json": "^11.0.0",
"prettier": "^3.6.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"prettier": "^3.6.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"redux": "^5.0.1",
"rollup": "^4.44.0",
"rollup": "^4.46.3",
"rollup-plugin-esbuild": "^6.2.1",
"shelljs": "^0.10.0",
"shx": "^0.4.0",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.1",
"typescript": "^5.9.2",
"typescript-eslint": "^8.43.0",
"use-sync-external-store": "^1.5.0",
"vitest": "^3.2.4"
},

1768
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -4,10 +4,10 @@ import type {
StoreMutatorIdentifier,
} from '../vanilla.ts'
export interface StateStorage {
export interface StateStorage<R = unknown> {
getItem: (name: string) => string | null | Promise<string | null>
setItem: (name: string, value: string) => unknown | Promise<unknown>
removeItem: (name: string) => unknown | Promise<unknown>
setItem: (name: string, value: string) => R
removeItem: (name: string) => R
}
export type StorageValue<S> = {
@ -15,12 +15,12 @@ export type StorageValue<S> = {
version?: number
}
export interface PersistStorage<S> {
export interface PersistStorage<S, R = unknown> {
getItem: (
name: string,
) => StorageValue<S> | null | Promise<StorageValue<S> | null>
setItem: (name: string, value: StorageValue<S>) => unknown | Promise<unknown>
removeItem: (name: string) => unknown | Promise<unknown>
setItem: (name: string, value: StorageValue<S>) => R
removeItem: (name: string) => R
}
type JsonStorageOptions = {
@ -28,18 +28,18 @@ type JsonStorageOptions = {
replacer?: (key: string, value: unknown) => unknown
}
export function createJSONStorage<S>(
getStorage: () => StateStorage,
export function createJSONStorage<S, R = unknown>(
getStorage: () => StateStorage<R>,
options?: JsonStorageOptions,
): PersistStorage<S> | undefined {
let storage: StateStorage | undefined
): PersistStorage<S, unknown> | undefined {
let storage: StateStorage<R> | undefined
try {
storage = getStorage()
} catch {
// prevent error if the storage is not defined (e.g. when server side rendering a page)
return
}
const persistStorage: PersistStorage<S> = {
const persistStorage: PersistStorage<S, R> = {
getItem: (name) => {
const parse = (str: string | null) => {
if (str === null) {
@ -60,7 +60,11 @@ export function createJSONStorage<S>(
return persistStorage
}
export interface PersistOptions<S, PersistedState = S> {
export interface PersistOptions<
S,
PersistedState = S,
PersistReturn = unknown,
> {
/** Name of the storage (must be unique) */
name: string
/**
@ -71,7 +75,7 @@ export interface PersistOptions<S, PersistedState = S> {
*
* @default createJSONStorage(() => localStorage)
*/
storage?: PersistStorage<PersistedState> | undefined
storage?: PersistStorage<PersistedState, PersistReturn> | undefined
/**
* Filter the persisted value.
*
@ -118,17 +122,28 @@ export interface PersistOptions<S, PersistedState = S> {
type PersistListener<S> = (state: S) => void
type StorePersist<S, Ps> = {
persist: {
setOptions: (options: Partial<PersistOptions<S, Ps>>) => void
clearStorage: () => void
rehydrate: () => Promise<void> | void
hasHydrated: () => boolean
onHydrate: (fn: PersistListener<S>) => () => void
onFinishHydration: (fn: PersistListener<S>) => () => void
getOptions: () => Partial<PersistOptions<S, Ps>>
type StorePersist<S, Ps, Pr> = S extends {
getState: () => infer T
setState: {
// capture both overloads of setState
(...args: infer Sa1): infer Sr1
(...args: infer Sa2): infer Sr2
}
}
? {
setState(...args: Sa1): Sr1 | Pr
setState(...args: Sa2): Sr2 | Pr
persist: {
setOptions: (options: Partial<PersistOptions<T, Ps, Pr>>) => void
clearStorage: () => void
rehydrate: () => Promise<void> | void
hasHydrated: () => boolean
onHydrate: (fn: PersistListener<T>) => () => void
onFinishHydration: (fn: PersistListener<T>) => () => void
getOptions: () => Partial<PersistOptions<T, Ps, Pr>>
}
}
: never
type Thenable<Value> = {
then<V>(
@ -172,7 +187,7 @@ const toThenable =
const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
type S = ReturnType<typeof config>
let options = {
storage: createJSONStorage<S>(() => localStorage),
storage: createJSONStorage<S, void>(() => localStorage),
partialize: (state: S) => state,
version: 0,
merge: (persistedState: unknown, currentState: S) => ({
@ -202,7 +217,7 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
const setItem = () => {
const state = options.partialize({ ...get() })
return (storage as PersistStorage<S>).setItem(options.name, {
return (storage as PersistStorage<S, unknown>).setItem(options.name, {
state,
version: options.version,
})
@ -212,13 +227,13 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
api.setState = (state, replace) => {
savedSetState(state, replace as any)
void setItem()
return setItem()
}
const configResult = config(
(...args) => {
set(...(args as Parameters<typeof set>))
void setItem()
return setItem()
},
get,
api,
@ -307,7 +322,7 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
})
}
;(api as StoreApi<S> & StorePersist<S, S>).persist = {
;(api as StoreApi<S> & StorePersist<StoreApi<S>, S, unknown>).persist = {
setOptions: (newOptions) => {
options = {
...options,
@ -365,9 +380,7 @@ declare module '../vanilla' {
type Write<T, U> = Omit<T, keyof U> & U
type WithPersist<S, A> = S extends { getState: () => infer T }
? Write<S, StorePersist<T, A>>
: never
type WithPersist<S, A> = Write<S, StorePersist<S, A, unknown>>
type PersistImpl = <T>(
storeInitializer: StateCreator<T, [], []>,

View File

@ -29,8 +29,8 @@ export function useStore<TState, StateSlice>(
) {
const slice = React.useSyncExternalStore(
api.subscribe,
() => selector(api.getState()),
() => selector(api.getInitialState()),
React.useCallback(() => selector(api.getState()), [api, selector]),
React.useCallback(() => selector(api.getInitialState()), [api, selector]),
)
React.useDebugValue(slice)
return slice

View File

@ -19,7 +19,7 @@ const compareEntries = (
return false
}
for (const [key, value] of mapA) {
if (!Object.is(value, mapB.get(key))) {
if (!mapB.has(key) || !Object.is(value, mapB.get(key))) {
return false
}
}

View File

@ -165,7 +165,9 @@ describe('persist middleware with async configuration', () => {
})
// Write something to the store
act(() => useBoundStore.setState({ count: 42 }))
act(() => {
useBoundStore.setState({ count: 42 })
})
expect(await screen.findByText('count: 42')).toBeInTheDocument()
expect(setItemSpy).toBeCalledWith(
'test-storage',
@ -788,7 +790,9 @@ describe('persist middleware with async configuration', () => {
// Write something to the store
const updatedMap = new Map(map).set('foo', 'bar')
act(() => useBoundStore.setState({ map: updatedMap }))
act(() => {
useBoundStore.setState({ map: updatedMap })
})
expect(await screen.findByText('map-content: bar')).toBeInTheDocument()
expect(setItemSpy).toBeCalledWith(

View File

@ -170,6 +170,10 @@ describe('shallow', () => {
const arr = [1, 2]
expect(shallow([arr, 1], [arr, 1])).toBe(true)
})
it('should work with undefined (#3204)', () => {
expect(shallow({ a: undefined }, { b: 1 })).toBe(false)
})
})
describe('mixed cases', () => {