Compare commits

...

8 Commits

Author SHA1 Message Date
David Brito
81df20a050
chore: fix dependabot config name (#3281) 2025-11-03 09:51:53 +09:00
Daishi Kato
18ab9e2615
chore: update dev dependencies (#3279)
* chore: update dev dependencies

* downgrade vitest

* packages entry
2025-11-01 12:32:05 +09:00
Daishi Kato
7ab97535d7
docs: add a note in nextjs guide (#3271) 2025-10-31 13:57:46 +09:00
Yurii Bezhentsev
77f5de91d6
docs: changed the name and title of the advanced TS guide (#3260)
* docs: changed name and title of the advanced TS guide

* fix readme links

---------

Co-authored-by: daishi <daishi@axlight.com>
2025-10-31 13:52:41 +09:00
Yurii Bezhentsev
d0c71ce15c
docs: created the new TypeScript Beginner Guide (#3246)
* docs: created the new TypeScript Beginner Guide

* docs: updated title

* docs: removed redundant sub-title

* docs: aligned indents

* docs: updated middleware titles

* docs: styled Conclusion section

* docs: added curried version of the "create" function

* docs: renamed the guide file

* docs: changed the title to have the same title format with "Advanced TypeScript Guide"

* docs: removed separating the curried form of the 'create' function

* docs: switched to create<T>()(...) and updated examples to make them more consistent

* docs: removed redundant TS store type extractor and unified middlewares

* docs: fixed format

* docs: fixed typo in the middleware list

* docs: updated doc according to the latest review. Also unified quotes

* docs: minor PR review fixes

* docs: removed Hooks section

* docs: added API documentation links to the corresponding sections

* Apply suggestions from code review

* fix format

---------

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
Co-authored-by: daishi <daishi@axlight.com>
2025-10-31 13:30:53 +09:00
thomas
d25a4d226f
docs and tests: filter out specific actions from being sent to Redux DevTools (#3252)
* feat:   filter out specific actions from being sent to Redux DevTools

* fix format issues

* refactor: rename actionBlacklist to actionsDenylist

* refactor: use redux devtools' type signature

* fix linter issue

* fix format issues

* refactor: update documentation

* refactor: format file

* Update src/middleware/devtools.ts

* fix: format issue

---------

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
2025-10-31 13:19:11 +09:00
Daishi Kato
ab5a98b187
test: fix for node 24 (#3278) 2025-10-31 13:17:00 +09:00
Danilo Britto
6422fa8898
Refactor useBoundStore and useLocalAndUrlStore (#3277) 2025-10-28 21:25:41 -05:00
16 changed files with 1419 additions and 1030 deletions

View File

@ -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

View File

@ -1,5 +1,5 @@
--- ---
title: TypeScript Guide title: Advanced TypeScript Guide
nav: 7 nav: 7
--- ---

View 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 well 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 stores 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, its 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 cant 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. Its 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.
Its 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 Zustands 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 cant 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 dont 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.

View File

@ -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: [],

View File

@ -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

View File

@ -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:

View File

@ -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' },

View File

@ -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

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,3 @@
packages:
- .
minimumReleaseAge: 1440

View File

@ -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 (

View File

@ -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.*'],
}),
)
})
})

View File

@ -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'

View File

@ -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'

View File

@ -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'