mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
Compare commits
24 Commits
c61999bacd
...
95850110ab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95850110ab | ||
|
|
20ad3f8891 | ||
|
|
56a39b6a0f | ||
|
|
612d5c4647 | ||
|
|
fd1ea8ca9d | ||
|
|
4db616e1f7 | ||
|
|
85e3f2929a | ||
|
|
0ea8be27db | ||
|
|
cadcd3eb91 | ||
|
|
feddc0c210 | ||
|
|
2cc19881fa | ||
|
|
c4085a4ff0 | ||
|
|
f831bc8d71 | ||
|
|
463c9e3ea6 | ||
|
|
30e36798a4 | ||
|
|
5aa923e8c3 | ||
|
|
41fed0fe6f | ||
|
|
aba78319bb | ||
|
|
2879505e39 | ||
|
|
1b7eb6907c | ||
|
|
84b112e4a2 | ||
|
|
4ad3977b38 | ||
|
|
56909808fa | ||
|
|
149c286342 |
4
.github/workflows/compressed-size.yml
vendored
4
.github/workflows/compressed-size.yml
vendored
@ -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'
|
||||
|
||||
4
.github/workflows/preview-release.yml
vendored
4
.github/workflows/preview-release.yml
vendored
@ -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'
|
||||
|
||||
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
@ -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'
|
||||
|
||||
4
.github/workflows/test-multiple-builds.yml
vendored
4
.github/workflows/test-multiple-builds.yml
vendored
@ -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'
|
||||
|
||||
8
.github/workflows/test-multiple-versions.yml
vendored
8
.github/workflows/test-multiple-versions.yml
vendored
@ -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'
|
||||
|
||||
5
.github/workflows/test-old-typescript.yml
vendored
5
.github/workflows/test-old-typescript.yml
vendored
@ -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'
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -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'
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
</p>
|
||||
|
||||
[](https://github.com/pmndrs/zustand/actions?query=workflow%3ALint)
|
||||
[](https://bundlephobia.com/result?p=zustand)
|
||||
[](https://bundlejs.com/?q=zustand)
|
||||
[](https://www.npmjs.com/package/zustand)
|
||||
[](https://www.npmjs.com/package/zustand)
|
||||
[](https://discord.gg/poimandres)
|
||||
|
||||
@ -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)
|
||||
- [I’ve updated the state, but the screen doesn’t update](#ive-updated-the-state-but-the-screen-doesnt-update)
|
||||
- [I’ve updated the state, but the screen doesn’t update](#i’ve-updated-the-state,-but-the-screen-doesn’t-update)
|
||||
|
||||
## Types
|
||||
|
||||
|
||||
@ -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>
|
||||
}
|
||||
```
|
||||
|
||||
@ -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 })),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
38
package.json
38
package.json
@ -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
1768
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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, [], []>,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user