mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
Compare commits
8 Commits
2a419514d1
...
81df20a050
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81df20a050 | ||
|
|
18ab9e2615 | ||
|
|
7ab97535d7 | ||
|
|
77f5de91d6 | ||
|
|
d0c71ce15c | ||
|
|
d25a4d226f | ||
|
|
ab5a98b187 | ||
|
|
6422fa8898 |
@ -487,7 +487,7 @@ const useBearStore = create<BearState>()(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
A more complete TypeScript guide is [here](docs/guides/typescript.md).
|
A more detailed TypeScript guide is [here](docs/guides/beginner-typescript.md) and [there](docs/guides/advanced-typescript.md).
|
||||||
|
|
||||||
## Best practices
|
## Best practices
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: TypeScript Guide
|
title: Advanced TypeScript Guide
|
||||||
nav: 7
|
nav: 7
|
||||||
---
|
---
|
||||||
|
|
||||||
378
docs/guides/beginner-typescript.md
Normal file
378
docs/guides/beginner-typescript.md
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
---
|
||||||
|
title: Beginner TypeScript Guide
|
||||||
|
nav: 7
|
||||||
|
---
|
||||||
|
|
||||||
|
Zustand is a lightweight state manager, particularly used with React. Zustand avoids reducers, context, and boilerplate.
|
||||||
|
Paired with TypeScript, you get a strongly typed store-state, actions, and selectors-with autocomplete and compile-time safety.
|
||||||
|
|
||||||
|
In this basic guide we’ll cover:
|
||||||
|
|
||||||
|
- Creating a typed store (state + actions)
|
||||||
|
- Using the store in React components with type safety
|
||||||
|
- Resetting the store safely with types
|
||||||
|
- Extracting and reusing Store type (for props, tests, and utilities)
|
||||||
|
- Composing multiple selectors and building derived state (with type inference and without extra re-renders)
|
||||||
|
- Middlewares with TypeScript support (`combine`, `devtools`, `persist`)
|
||||||
|
- Async actions with typed API responses
|
||||||
|
- Working with `createWithEqualityFn` (enhanced `create` store function)
|
||||||
|
- Structuring and coordinating multiple stores
|
||||||
|
|
||||||
|
### Creating a Store with State & Actions
|
||||||
|
|
||||||
|
Here we describe state and actions using an Typescript interface. The `<BearState>` generic forces the store to match this shape.
|
||||||
|
This means if you forget a field or use the wrong type, TypeScript will complain. Unlike plain JS, this guarantees type-safe state management.
|
||||||
|
The `create` function uses the curried form, which results in a store of type `UseBoundStore<StoreApi<BearState>>`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// store.ts
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
// Define types for state & actions
|
||||||
|
interface BearState {
|
||||||
|
bears: number
|
||||||
|
food: string
|
||||||
|
feed: (food: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create store using the curried form of `create`
|
||||||
|
export const useBearStore = create<BearState>()((set) => ({
|
||||||
|
bears: 2,
|
||||||
|
food: 'honey',
|
||||||
|
feed: (food) => set(() => ({ food })),
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the Store in Components
|
||||||
|
|
||||||
|
Inside components, you can read state and call actions. Selectors `(s) => s.bears` subscribe to only what you need.
|
||||||
|
This reduces re-renders and improves performance. JS can do this too, but with TS your IDE autocompletes state fields.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useBearStore } from './store'
|
||||||
|
|
||||||
|
function BearCounter() {
|
||||||
|
// Select only 'bears' to avoid unnecessary re-renders
|
||||||
|
const bears = useBearStore((s) => s.bears)
|
||||||
|
return <h1>{bears} bears around</h1>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resetting the Store
|
||||||
|
|
||||||
|
Resetting is useful after logout or “clear session”. We use `typeof initialState` to avoid repeating property types.
|
||||||
|
TypeScript updates automatically if `initialState` changes. This is safer and cleaner compared to JS.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
const initialState = { bears: 0, food: 'honey' }
|
||||||
|
|
||||||
|
// Reuse state type dynamically
|
||||||
|
type BearState = typeof initialState & {
|
||||||
|
increase: (by: number) => void
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBearStore = create<BearState>()((set) => ({
|
||||||
|
...initialState,
|
||||||
|
increase: (by) => set((s) => ({ bears: s.bears + by })),
|
||||||
|
reset: () => set(initialState),
|
||||||
|
}))
|
||||||
|
|
||||||
|
function ResetZoo() {
|
||||||
|
const { bears, increase, reset } = useBearStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>{bears}</div>
|
||||||
|
<button onClick={() => increase(5)}>Increase by 5</button>
|
||||||
|
<button onClick={reset}>Reset</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extracting Types
|
||||||
|
|
||||||
|
Zustand provides a built-in helper called `ExtractState`. This is useful for tests, utility functions, or component props.
|
||||||
|
It returns the full type of your store’s state and actions without having to manually redefine them. Extracting the Store type:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// store.ts
|
||||||
|
import { create, type ExtractState } from 'zustand'
|
||||||
|
|
||||||
|
export const useBearStore = create((set) => ({
|
||||||
|
bears: 3,
|
||||||
|
food: 'honey',
|
||||||
|
increase: (by: number) => set((s) => ({ bears: s.bears + by })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Extract the type of the whole store state
|
||||||
|
export type BearState = ExtractState<typeof useBearStore>
|
||||||
|
```
|
||||||
|
|
||||||
|
Using extracted type in tests:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// test.cy.ts
|
||||||
|
import { BearState } from './store.ts'
|
||||||
|
|
||||||
|
test('should reset store', () => {
|
||||||
|
const snapshot: BearState = useBearStore.getState()
|
||||||
|
expect(snapshot.bears).toBeGreaterThanOrEqual(0)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
and in utility function:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// util.ts
|
||||||
|
import { BearState } from './store.ts'
|
||||||
|
|
||||||
|
function logBearState(state: BearState) {
|
||||||
|
console.log(`We have ${state.bears} bears eating ${state.food}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
logBearState(useBearStore.getState())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Selectors
|
||||||
|
|
||||||
|
#### Multiple Selectors
|
||||||
|
|
||||||
|
Sometimes you need more than one property. Returning an object from the selector lets you access multiple fields at once.
|
||||||
|
However, directly destructuring properties from that object can cause unnecessary re-renders.
|
||||||
|
To avoid this, it’s recommended to wrap the selector with `useShallow`, which prevents re-renders when the selected values remain shallowly equal.
|
||||||
|
This is more efficient than subscribing to the whole store. TypeScript ensures you can’t accidentally misspell `bears` or `food`.
|
||||||
|
See the [API documentation](https://zustand.docs.pmnd.rs/hooks/use-shallow) for more details on `useShallow`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
|
// Bear store with explicit types
|
||||||
|
interface BearState {
|
||||||
|
bears: number
|
||||||
|
food: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBearStore = create<BearState>()(() => ({
|
||||||
|
bears: 2,
|
||||||
|
food: 10,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// In components, you can use both stores safely
|
||||||
|
function MultipleSelectors() {
|
||||||
|
const { bears, food } = useBearStore(
|
||||||
|
useShallow((state) => ({ bears: state.bears, food: state.food })),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
We have {food} units of food for {bears} bears
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Derived State with Selectors
|
||||||
|
|
||||||
|
Not all values need to be stored directly - some can be computed from existing state. You can derive values using selectors.
|
||||||
|
This avoids duplication and keeps the store minimal. TypeScript ensures `bears` is a number, so math is safe.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface BearState {
|
||||||
|
bears: number
|
||||||
|
foodPerBear: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBearStore = create<BearState>()(() => ({
|
||||||
|
bears: 3,
|
||||||
|
foodPerBear: 2,
|
||||||
|
}))
|
||||||
|
|
||||||
|
function TotalFood() {
|
||||||
|
// Derived value: required amount food for all bears
|
||||||
|
const totalFood = useBearStore((s) => s.bears * s.foodPerBear) // don't need to have extra property `{ totalFood: 6 }` in your Store
|
||||||
|
|
||||||
|
return <div>We need ${totalFood} jars of honey</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Middlewares
|
||||||
|
|
||||||
|
#### `combine` middleware
|
||||||
|
|
||||||
|
This middleware separates initial state and actions, making the code cleaner.
|
||||||
|
TS automatically infers types from the state and actions, no interface needed.
|
||||||
|
This is different from JS, where type safety is missing. It’s a very popular style in TypeScript projects.
|
||||||
|
See the [API documentation](https://zustand.docs.pmnd.rs/middlewares/combine) for more details.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { combine } from 'zustand/middleware'
|
||||||
|
|
||||||
|
interface BearState {
|
||||||
|
bears: number
|
||||||
|
increase: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// State + actions are separated
|
||||||
|
export const useBearStore = create<BearState>()(
|
||||||
|
combine({ bears: 0 }, (set) => ({
|
||||||
|
increase: () => set((s) => ({ bears: s.bears + 1 })),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `devtools` middleware
|
||||||
|
|
||||||
|
This middleware connects Zustand to Redux DevTools. You can inspect changes, time-travel, and debug state.
|
||||||
|
It’s extremely useful in development. TS ensures your actions and state remain type-checked even here.
|
||||||
|
See the [API documentation](https://zustand.docs.pmnd.rs/middlewares/devtools) for more details.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { devtools } from 'zustand/middleware'
|
||||||
|
|
||||||
|
interface BearState {
|
||||||
|
bears: number
|
||||||
|
increase: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBearStore = create<BearState>()(
|
||||||
|
devtools((set) => ({
|
||||||
|
bears: 0,
|
||||||
|
increase: () => set((s) => ({ bears: s.bears + 1 })),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `persist` middleware
|
||||||
|
|
||||||
|
This middleware keeps your store in `localStorage` (or another storage). This means your bears survive a page refresh.
|
||||||
|
Great for apps where persistence matters. In TS, the state type stays consistent, so no runtime surprises.
|
||||||
|
See the [API documentation](https://zustand.docs.pmnd.rs/middlewares/persist) for more details.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { persist } from 'zustand/middleware'
|
||||||
|
|
||||||
|
interface BearState {
|
||||||
|
bears: number
|
||||||
|
increase: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBearStore = create<BearState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
bears: 0,
|
||||||
|
increase: () => set((s) => ({ bears: s.bears + 1 })), // <-- тип явно
|
||||||
|
}),
|
||||||
|
{ name: 'bear-storage' }, // localStorage key
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Actions
|
||||||
|
|
||||||
|
Actions can be async to fetch remote data. Here we fetch bears count and update state.
|
||||||
|
TS enforces correct API response type (`BearData`). In JS you might misspell `count` - TS prevents that.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface BearData {
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BearState {
|
||||||
|
bears: number
|
||||||
|
fetchBears: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBearStore = create<BearState>()((set) => ({
|
||||||
|
bears: 0,
|
||||||
|
fetchBears: async () => {
|
||||||
|
const res = await fetch('/api/bears')
|
||||||
|
const data: BearData = await res.json()
|
||||||
|
|
||||||
|
set({ bears: data.count })
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### `createWithEqualityFn`
|
||||||
|
|
||||||
|
Variant of `create` with equality built-in. Useful if you always want custom equality checks.
|
||||||
|
Not common, but shows Zustand’s flexibility. TS still keeps full type inference.
|
||||||
|
See the [API documentation](https://zustand.docs.pmnd.rs/apis/create-with-equality-fn) for more details.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createWithEqualityFn } from 'zustand/traditional'
|
||||||
|
import { shallow } from 'zustand/shallow'
|
||||||
|
|
||||||
|
const useBearStore = createWithEqualityFn(() => ({
|
||||||
|
bears: 0,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const bears = useBearStore((s) => s.bears, Object.is)
|
||||||
|
// or
|
||||||
|
const bears = useBearStore((s) => ({ bears: s.bears }), shallow)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Stores
|
||||||
|
|
||||||
|
You can create more than one store for different domains. For example, `BearStore` manages bears and `FishStore` manages fish.
|
||||||
|
This keeps state isolated and easier to maintain in larger apps. With TypeScript, each store has its own strict type - you can’t accidentally mix bears and fish.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
// Bear store with explicit types
|
||||||
|
interface BearState {
|
||||||
|
bears: number
|
||||||
|
addBear: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBearStore = create<BearState>()((set) => ({
|
||||||
|
bears: 2,
|
||||||
|
addBear: () => set((s) => ({ bears: s.bears + 1 })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Fish store with explicit types
|
||||||
|
interface FishState {
|
||||||
|
fish: number
|
||||||
|
addFish: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFishStore = create<FishState>()((set) => ({
|
||||||
|
fish: 5,
|
||||||
|
addFish: () => set((s) => ({ fish: s.fish + 1 })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// In components, you can use both stores safely
|
||||||
|
function Zoo() {
|
||||||
|
const { bears, addBear } = useBearStore()
|
||||||
|
const { fish, addFish } = useFishStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{bears} bears and {fish} fish
|
||||||
|
</div>
|
||||||
|
<button onClick={addBear}>Add bear</button>
|
||||||
|
<button onClick={addFish}>Add fish</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conclusion
|
||||||
|
|
||||||
|
Zustand together with TypeScript provides a balance: you keep the simplicity of small, minimalistic stores, while gaining the safety of strong typing.
|
||||||
|
You don’t need boilerplate or complex patterns - state and actions live side by side, fully typed, and ready to use.
|
||||||
|
Start with a basic store to learn the pattern, then expand gradually: use `combine` for cleaner inference, `persist` for storage, and `devtools` for debugging.
|
||||||
@ -29,7 +29,7 @@ const hashStorage: StateStorage = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useBoundStore = create(
|
export const useBoundStore = create()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
fishes: 0,
|
fishes: 0,
|
||||||
@ -102,7 +102,7 @@ const storageOptions = {
|
|||||||
storage: createJSONStorage<LocalAndUrlStore>(() => persistentStorage),
|
storage: createJSONStorage<LocalAndUrlStore>(() => persistentStorage),
|
||||||
}
|
}
|
||||||
|
|
||||||
const useLocalAndUrlStore = create(
|
const useLocalAndUrlStore = create()(
|
||||||
persist<LocalAndUrlStore>(
|
persist<LocalAndUrlStore>(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
typesOfFish: [],
|
typesOfFish: [],
|
||||||
|
|||||||
@ -3,6 +3,9 @@ title: Setup with Next.js
|
|||||||
nav: 17
|
nav: 17
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We will be updating this guide soon based on our discussion in https://github.com/pmndrs/zustand/discussions/2740.
|
||||||
|
|
||||||
[Next.js](https://nextjs.org) is a popular server-side rendering framework for React that presents
|
[Next.js](https://nextjs.org) is a popular server-side rendering framework for React that presents
|
||||||
some unique challenges for using Zustand properly.
|
some unique challenges for using Zustand properly.
|
||||||
Keep in mind that Zustand store is a global
|
Keep in mind that Zustand store is a global
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const nextStateCreatorFn = devtools(stateCreatorFn, devtoolsOptions)
|
|||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Debugging a store](#debugging-a-store)
|
- [Debugging a store](#debugging-a-store)
|
||||||
- [Debugging a Slices pattern based store](#debugging-a-slices-pattern-based-store)
|
- [Debugging a Slices pattern based store](#debugging-a-slices-pattern-based-store)
|
||||||
|
- [Filtering actions with actionsDenylist](#filtering-actions-with-actionsdenylist)
|
||||||
- [Cleanup](#cleanup)
|
- [Cleanup](#cleanup)
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Troubleshooting](#troubleshooting)
|
||||||
- [Only one store is displayed](#only-one-store-is-displayed)
|
- [Only one store is displayed](#only-one-store-is-displayed)
|
||||||
@ -61,6 +62,9 @@ devtools<T>(stateCreatorFn: StateCreator<T, [], []>, devtoolsOptions?: DevtoolsO
|
|||||||
- **optional** `anonymousActionType`: Defaults to the inferred action type or `anonymous` if
|
- **optional** `anonymousActionType`: Defaults to the inferred action type or `anonymous` if
|
||||||
unavailable. A string to use as the action type for anonymous mutations in the Redux DevTools.
|
unavailable. A string to use as the action type for anonymous mutations in the Redux DevTools.
|
||||||
- **optional** `store`: A custom identifier for the store in the Redux DevTools.
|
- **optional** `store`: A custom identifier for the store in the Redux DevTools.
|
||||||
|
- **optional** `actionsDenylist`: A string or array of strings (regex patterns) that specify which
|
||||||
|
actions should be filtered out from Redux DevTools. This option is passed directly to Redux DevTools
|
||||||
|
for filtering. For example, `['secret.*']` will filter out all actions starting with "secret".
|
||||||
|
|
||||||
#### Returns
|
#### Returns
|
||||||
|
|
||||||
@ -157,6 +161,61 @@ const useJungleStore = create<JungleStore>()(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Filtering actions with actionsDenylist
|
||||||
|
|
||||||
|
You can filter out specific actions from Redux DevTools using the `actionsDenylist` option. This is useful for hiding internal or sensitive actions from the DevTools timeline.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { devtools } from 'zustand/middleware'
|
||||||
|
|
||||||
|
type Store = {
|
||||||
|
user: string | null
|
||||||
|
token: string | null
|
||||||
|
login: (user: string, token: string) => void
|
||||||
|
logout: () => void
|
||||||
|
updateData: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStore = create<Store>()(
|
||||||
|
devtools(
|
||||||
|
(set) => ({
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
login: (user, token) => set({ user, token }, undefined, 'auth/login'),
|
||||||
|
logout: () => set({ user: null, token: null }, undefined, 'auth/logout'),
|
||||||
|
updateData: () =>
|
||||||
|
set({ user: 'updated' }, undefined, 'internal/updateData'),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'AuthStore',
|
||||||
|
// Filter out actions matching these regex patterns
|
||||||
|
actionsDenylist: ['internal/.*'], // Hides all 'internal/*' actions
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use a single regex string:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const useStore = create<Store>()(
|
||||||
|
devtools(
|
||||||
|
(set) => ({
|
||||||
|
// ... state and actions
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'MyStore',
|
||||||
|
actionsDenylist: 'secret.*', // Hides all actions starting with 'secret'
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The `actionsDenylist` option uses regex pattern matching and is handled directly by Redux DevTools Extension.
|
||||||
|
> All actions are still sent to DevTools, but matching actions are filtered from the display.
|
||||||
|
|
||||||
### Cleanup
|
### Cleanup
|
||||||
|
|
||||||
When a store is no longer needed, you can clean up the Redux DevTools connection by calling the `cleanup` method on the store:
|
When a store is no longer needed, you can clean up the Redux DevTools connection by calling the `cleanup` method on the store:
|
||||||
|
|||||||
@ -10,14 +10,14 @@ import tseslint from 'typescript-eslint'
|
|||||||
|
|
||||||
export default defineConfig(
|
export default defineConfig(
|
||||||
{
|
{
|
||||||
ignores: ['dist/', 'examples/'],
|
ignores: ['dist/', 'examples/', 'website/'],
|
||||||
},
|
},
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
importPlugin.flatConfigs.recommended,
|
importPlugin.flatConfigs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
react.configs.flat.recommended,
|
react.configs.flat.recommended,
|
||||||
react.configs.flat['jsx-runtime'],
|
react.configs.flat['jsx-runtime'],
|
||||||
reactHooks.configs.recommended,
|
reactHooks.configs.flat.recommended,
|
||||||
{
|
{
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
@ -71,7 +71,6 @@ export default defineConfig(
|
|||||||
'error',
|
'error',
|
||||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||||
],
|
],
|
||||||
'react-hooks/react-compiler': 'error',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -81,7 +80,6 @@ export default defineConfig(
|
|||||||
...vitest.configs.recommended,
|
...vitest.configs.recommended,
|
||||||
rules: {
|
rules: {
|
||||||
'import/extensions': ['error', 'never'],
|
'import/extensions': ['error', 'never'],
|
||||||
'testing-library/no-node-access': 'off',
|
|
||||||
'vitest/consistent-test-it': [
|
'vitest/consistent-test-it': [
|
||||||
'error',
|
'error',
|
||||||
{ fn: 'it', withinDescribe: 'it' },
|
{ fn: 'it', withinDescribe: 'it' },
|
||||||
|
|||||||
46
package.json
46
package.json
@ -114,46 +114,46 @@
|
|||||||
"url": "https://github.com/pmndrs/zustand/issues"
|
"url": "https://github.com/pmndrs/zustand/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/pmndrs/zustand",
|
"homepage": "https://github.com/pmndrs/zustand",
|
||||||
"packageManager": "pnpm@10.15.0",
|
"packageManager": "pnpm@10.18.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.38.0",
|
||||||
"@redux-devtools/extension": "^3.3.0",
|
"@redux-devtools/extension": "^3.3.0",
|
||||||
"@rollup/plugin-alias": "^5.1.1",
|
"@rollup/plugin-alias": "^6.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||||
"@rollup/plugin-replace": "^6.0.2",
|
"@rollup/plugin-replace": "^6.0.3",
|
||||||
"@rollup/plugin-typescript": "12.1.4",
|
"@rollup/plugin-typescript": "12.3.0",
|
||||||
"@testing-library/jest-dom": "^6.7.0",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.9.2",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@types/use-sync-external-store": "^1.5.0",
|
"@types/use-sync-external-store": "^1.5.0",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/eslint-plugin": "^1.3.4",
|
"@vitest/eslint-plugin": "^1.3.26",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"esbuild": "^0.25.9",
|
"esbuild": "^0.25.11",
|
||||||
"eslint": "9.33.0",
|
"eslint": "9.38.0",
|
||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jest-dom": "^5.5.0",
|
"eslint-plugin-jest-dom": "^5.5.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "6.0.0-rc.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-testing-library": "^7.6.6",
|
"eslint-plugin-testing-library": "^7.13.3",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.2.0",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^27.0.1",
|
||||||
"json": "^11.0.0",
|
"json": "^11.0.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"react": "19.1.1",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.2.0",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"rollup": "^4.46.3",
|
"rollup": "^4.52.5",
|
||||||
"rollup-plugin-esbuild": "^6.2.1",
|
"rollup-plugin-esbuild": "^6.2.1",
|
||||||
"shelljs": "^0.10.0",
|
"shelljs": "^0.10.0",
|
||||||
"shx": "^0.4.0",
|
"shx": "^0.4.0",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.43.0",
|
"typescript-eslint": "^8.46.2",
|
||||||
"use-sync-external-store": "^1.5.0",
|
"use-sync-external-store": "^1.6.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
1906
pnpm-lock.yaml
generated
1906
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
- .
|
||||||
|
minimumReleaseAge: 1440
|
||||||
@ -645,6 +645,7 @@ it('ensures the correct subscriber is removed on unmount', async () => {
|
|||||||
function Component() {
|
function Component() {
|
||||||
const [Counter, setCounter] = useState(() => CountWithInitialIncrement)
|
const [Counter, setCounter] = useState(() => CountWithInitialIncrement)
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setCounter(() => Count)
|
setCounter(() => Count)
|
||||||
}, [])
|
}, [])
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -218,7 +218,7 @@ describe('When state changes with automatic setter inferring...', () => {
|
|||||||
api.getState().setCount(10)
|
api.getState().setCount(10)
|
||||||
const [connection] = getNamedConnectionApis(options.name)
|
const [connection] = getNamedConnectionApis(options.name)
|
||||||
expect(connection.send).toHaveBeenLastCalledWith(
|
expect(connection.send).toHaveBeenLastCalledWith(
|
||||||
{ type: 'Object.setCount' },
|
{ type: expect.stringMatching(/^(Object\.setCount|anonymous)$/) },
|
||||||
{ count: 10, setCount: expect.any(Function) },
|
{ count: 10, setCount: expect.any(Function) },
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -669,25 +669,25 @@ describe('different envs', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('works in non-browser env', async () => {
|
it('works in non-browser env', async () => {
|
||||||
const originalWindow = global.window
|
const originalWindow = globalThis.window
|
||||||
global.window = undefined as any
|
globalThis.window = undefined as any
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
createStore(devtools(() => ({ count: 0 }), { enabled: true }))
|
createStore(devtools(() => ({ count: 0 }), { enabled: true }))
|
||||||
}).not.toThrow()
|
}).not.toThrow()
|
||||||
|
|
||||||
global.window = originalWindow
|
globalThis.window = originalWindow
|
||||||
})
|
})
|
||||||
|
|
||||||
it('works in react native env', async () => {
|
it('works in react native env', async () => {
|
||||||
const originalWindow = global.window
|
const originalWindow = globalThis.window
|
||||||
global.window = {} as any
|
globalThis.window = {} as any
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
createStore(devtools(() => ({ count: 0 }), { enabled: true }))
|
createStore(devtools(() => ({ count: 0 }), { enabled: true }))
|
||||||
}).not.toThrow()
|
}).not.toThrow()
|
||||||
|
|
||||||
global.window = originalWindow
|
globalThis.window = originalWindow
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -2520,3 +2520,23 @@ describe('cleanup', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('actionsDenylist', () => {
|
||||||
|
it('should pass actionsDenylist option to Redux DevTools', async () => {
|
||||||
|
const options = {
|
||||||
|
name: 'test-filter',
|
||||||
|
enabled: true,
|
||||||
|
actionsDenylist: ['secret.*'],
|
||||||
|
}
|
||||||
|
|
||||||
|
createStore(devtools(() => ({ count: 0 }), options))
|
||||||
|
|
||||||
|
// Verify that actionsDenylist was passed to the connect call
|
||||||
|
const extensionConnector = (window as any).__REDUX_DEVTOOLS_EXTENSION__
|
||||||
|
expect(extensionConnector.connect).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
actionsDenylist: ['secret.*'],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
/* eslint @typescript-eslint/no-unused-expressions: off */ // FIXME
|
/* eslint @typescript-eslint/no-unused-expressions: off */ // FIXME
|
||||||
/* eslint react-hooks/react-compiler: off */
|
|
||||||
|
|
||||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
|
||||||
import { StrictMode, useEffect } from 'react'
|
import { StrictMode, useEffect } from 'react'
|
||||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { createJSONStorage, persist } from 'zustand/middleware'
|
import { createJSONStorage, persist } from 'zustand/middleware'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user