mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
Merge branch 'main' into v5
This commit is contained in:
commit
7868b8c005
4
.github/workflows/test-multiple-versions.yml
vendored
4
.github/workflows/test-multiple-versions.yml
vendored
@ -31,8 +31,8 @@ jobs:
|
||||
- 18.0.0
|
||||
- 18.1.0
|
||||
- 18.2.0
|
||||
- 18.3.0-canary-feed8f3f9-20240118
|
||||
- 0.0.0-experimental-feed8f3f9-20240118
|
||||
- 18.3.0-canary-a9cc32511-20240215
|
||||
- 0.0.0-experimental-a9cc32511-20240215
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
399
docs/guides/nextjs.md
Normal file
399
docs/guides/nextjs.md
Normal file
@ -0,0 +1,399 @@
|
||||
---
|
||||
title: Setup with Next.js
|
||||
nav: 21
|
||||
---
|
||||
|
||||
[Next.js](https://nextjs.org) is a popular server-side rendering framework for React that presents
|
||||
some unique challenges for using Zustand properly.
|
||||
Keep in mind that Zustand store is a global
|
||||
variable (AKA module state) making it optional to use a `Context`.
|
||||
These challenges include:
|
||||
|
||||
- **Per-request store:** A Next.js server can handle multiple requests simultaneously. This means
|
||||
that the store should be created per request and should not be shared across requests.
|
||||
- **SSR friendly:** Next.js applications are rendered twice, first on the server
|
||||
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.
|
||||
- **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 intialize it at the component level using a
|
||||
`Context`.
|
||||
- **Server caching friendly:** Recent versions of Next.js (specifically applications using the App
|
||||
Router architecture) support aggressive server caching. Due to our store being a **module state**,
|
||||
it is completely compatible with this caching.
|
||||
|
||||
We have these general recommendations for the appropriate use of Zustand:
|
||||
|
||||
- **No global stores** - Because the store should not be shared across requests, it should not be defined
|
||||
as a global variable. Instead, the store should be created per request.
|
||||
- **React Server Components should not read from or write to the store** - RSCs cannot use hooks or context. They aren't
|
||||
meant to be stateful. Having an RSC read from or write values to a global store violates the
|
||||
architecture of Next.js.
|
||||
|
||||
### Creating a store per request
|
||||
|
||||
Let's write our store factory function that will create a new store for each
|
||||
request.
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** do not forget to remove all comments from your `tsconfig.json` file.
|
||||
|
||||
```ts
|
||||
// src/stores/counter-store.ts
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
|
||||
export type CounterState = {
|
||||
count: number
|
||||
}
|
||||
|
||||
export type CounterActions = {
|
||||
decrementCount: () => void
|
||||
incrementCount: () => void
|
||||
}
|
||||
|
||||
export type CounterStore = CounterState & CounterActions
|
||||
|
||||
export const defaultInitState: CounterState = {
|
||||
count: 0,
|
||||
}
|
||||
|
||||
export const createCounterStore = (
|
||||
initState: CounterState = defaultInitState,
|
||||
) => {
|
||||
return createStore<CounterStore>()((set) => ({
|
||||
...initState,
|
||||
decrementCount: () => set((state) => ({ count: state.count - 1 })),
|
||||
incrementCount: () => set((state) => ({ count: state.count + 1 })),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### Providing the store
|
||||
|
||||
Let's use the `createCounterStore` in our component and share it using a context provider.
|
||||
|
||||
```tsx
|
||||
// src/providers/counter-store-provider.tsx
|
||||
'use client'
|
||||
|
||||
import { type ReactNode, createContext, useRef, useContext } from 'react'
|
||||
import { type StoreApi, useStore } from 'zustand'
|
||||
|
||||
import { type CounterStore, createCounterStore } from '@/stores/counter-store'
|
||||
|
||||
export const CounterStoreContext = createContext<StoreApi<CounterStore> | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
export interface CounterStoreProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const CounterStoreProvider = ({
|
||||
children,
|
||||
}: CounterStoreProviderProps) => {
|
||||
const storeRef = useRef<StoreApi<CounterStore>>()
|
||||
if (!storeRef.current) {
|
||||
storeRef.current = createCounterStore()
|
||||
}
|
||||
|
||||
return (
|
||||
<CounterStoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</CounterStoreContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCounterStore = <T,>(
|
||||
selector: (store: CounterStore) => T,
|
||||
): T => {
|
||||
const counterStoreContext = useContext(CounterStoreContext)
|
||||
|
||||
if (!counterStoreContext) {
|
||||
throw new Error(`useCounterStore must be use within CounterStoreProvider`)
|
||||
}
|
||||
|
||||
return useStore(counterStoreContext, selector)
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** In this example, we ensure that this component is re-render-safe by checking the
|
||||
> value of the reference, so that the store is only created once. This component will only be
|
||||
> rendered once per request on the server, but might be re-rendered multiple times on the client if
|
||||
> there are stateful client components located above this component in the tree, or if this component
|
||||
> also contains other mutable state that causes a re-render.
|
||||
|
||||
### Initializing the store
|
||||
|
||||
```ts
|
||||
// src/stores/counter-store.ts
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
|
||||
export type CounterState = {
|
||||
count: number
|
||||
}
|
||||
|
||||
export type CounterActions = {
|
||||
decrementCount: () => void
|
||||
incrementCount: () => void
|
||||
}
|
||||
|
||||
export type CounterStore = CounterState & CounterActions
|
||||
|
||||
export const initCounterStore = (): CounterState => {
|
||||
return { count: new Date().getFullYear() }
|
||||
}
|
||||
|
||||
export const defaultInitState: CounterState = {
|
||||
count: 0,
|
||||
}
|
||||
|
||||
export const createCounterStore = (
|
||||
initState: CounterState = defaultInitState,
|
||||
) => {
|
||||
return createStore<CounterStore>()((set) => ({
|
||||
...initState,
|
||||
decrementCount: () => set((state) => ({ count: state.count - 1 })),
|
||||
incrementCount: () => set((state) => ({ count: state.count + 1 })),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// src/providers/counter-store-provider.tsx
|
||||
'use client'
|
||||
|
||||
import { type ReactNode, createContext, useRef, useContext } from 'react'
|
||||
import { type StoreApi, useStore } from 'zustand'
|
||||
|
||||
import {
|
||||
type CounterStore,
|
||||
createCounterStore,
|
||||
initCounterStore,
|
||||
} from '@/stores/counter-store'
|
||||
|
||||
export const CounterStoreContext = createContext<StoreApi<CounterStore> | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
export interface CounterStoreProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const CounterStoreProvider = ({
|
||||
children,
|
||||
}: CounterStoreProviderProps) => {
|
||||
const storeRef = useRef<StoreApi<CounterStore>>()
|
||||
if (!storeRef.current) {
|
||||
storeRef.current = createCounterStore(initCounterStore())
|
||||
}
|
||||
|
||||
return (
|
||||
<CounterStoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</CounterStoreContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCounterStore = <T,>(
|
||||
selector: (store: CounterStore) => T,
|
||||
): T => {
|
||||
const counterStoreContext = useContext(CounterStoreContext)
|
||||
|
||||
if (!counterStoreContext) {
|
||||
throw new Error(`useCounterStore must be use within CounterStoreProvider`)
|
||||
}
|
||||
|
||||
return useStore(counterStoreContext, selector)
|
||||
}
|
||||
```
|
||||
|
||||
### Using the store with different architectures
|
||||
|
||||
There are two architectures for a Next.js application: the
|
||||
[Pages Router](https://nextjs.org/docs/pages/building-your-application/routing) and the
|
||||
[App Router](https://nextjs.org/docs/app/building-your-application/routing). The usage of Zustand on
|
||||
both architectures should be the same with slight differences related to each architecture.
|
||||
|
||||
#### Pages Router
|
||||
|
||||
```tsx
|
||||
// src/components/pages/home-page.tsx
|
||||
import { useCounterStore } from '@/providers/counter-store-provider.ts'
|
||||
|
||||
export const HomePage = () => {
|
||||
const { count, incrementCount, decrementCount } = useCounterStore(
|
||||
(state) => state,
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
Count: {count}
|
||||
<hr />
|
||||
<button type="button" onClick={() => void incrementCount()}>
|
||||
Increment Count
|
||||
</button>
|
||||
<button type="button" onClick={() => void decrementCount()}>
|
||||
Decrement Count
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// src/_app.tsx
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
import { CounterStoreProvider } from '@/providers/counter-store-provider.tsx'
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<CounterStoreProvider>
|
||||
<Component {...pageProps} />
|
||||
</CounterStoreProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// src/pages/index.tsx
|
||||
import { HomePage } from '@/components/pages/home-page.tsx'
|
||||
|
||||
export default function Home() {
|
||||
return <HomePage />
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** creating a store per route would require creating and sharing the store
|
||||
> at page (route) component level. Try not to use this if you do not need to create
|
||||
> a store per route.
|
||||
|
||||
```tsx
|
||||
// src/pages/index.tsx
|
||||
import { CounterStoreProvider } from '@/providers/counter-store-provider.tsx'
|
||||
import { HomePage } from '@/components/pages/home-page.tsx'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<CounterStoreProvider>
|
||||
<HomePage />
|
||||
</CounterStoreProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### App Router
|
||||
|
||||
```tsx
|
||||
// src/components/pages/home-page.tsx
|
||||
'use client'
|
||||
|
||||
import { useCounterStore } from '@/providers/counter-store-provider'
|
||||
|
||||
export const HomePage = () => {
|
||||
const { count, incrementCount, decrementCount } = useCounterStore(
|
||||
(state) => state,
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
Count: {count}
|
||||
<hr />
|
||||
<button type="button" onClick={() => void incrementCount()}>
|
||||
Increment Count
|
||||
</button>
|
||||
<button type="button" onClick={() => void decrementCount()}>
|
||||
Decrement Count
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// src/app/layout.tsx
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
import { CounterStoreProvider } from '@/providers/counter-store-provider'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<CounterStoreProvider>{children}</CounterStoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// src/app/page.tsx
|
||||
import { HomePage } from '@/components/pages/home-page'
|
||||
|
||||
export default function Home() {
|
||||
return <HomePage />
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** creating a store per route would require creating and sharing the store
|
||||
> at page (route) component level. Try not to use this if you do not need to create
|
||||
> a store per route.
|
||||
|
||||
```tsx
|
||||
// src/app/page.tsx
|
||||
import { CounterStoreProvider } from '@/providers/counter-store-provider'
|
||||
import { HomePage } from '@/components/pages/home-page'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<CounterStoreProvider>
|
||||
<HomePage />
|
||||
</CounterStoreProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
198
docs/guides/ssr-and-hydration.md
Normal file
198
docs/guides/ssr-and-hydration.md
Normal file
@ -0,0 +1,198 @@
|
||||
---
|
||||
title: SSR and Hydration
|
||||
nav: 20
|
||||
---
|
||||
|
||||
## Server-side Rendering (SSR)
|
||||
|
||||
Server-side Rendering (SSR) is a technique that helps us render our components into
|
||||
HTML strings on the server, send them directly to the browser, and finally "hydrate" the
|
||||
static markup into a fully interactive app on the client.
|
||||
|
||||
### React
|
||||
|
||||
Let's say we want to render a stateless app using React. In order to do that, we need
|
||||
to use `express`, `react` and `react-dom/server`. We don't need `react-dom/client`
|
||||
since it's a stateless app.
|
||||
|
||||
Let's dive into that:
|
||||
|
||||
- `express` helps us build a web app that we can run using Node,
|
||||
- `react` helps us build the UI components that we use in our app,
|
||||
- `react-dom/server` helps us render our components on a server.
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"noEmitOnError": true,
|
||||
"removeComments": false,
|
||||
"sourceMap": true,
|
||||
"target": "esnext"
|
||||
},
|
||||
"include": ["**/*"]
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** do not forget to remove all comments from your `tsconfig.json` file.
|
||||
|
||||
```tsx
|
||||
// app.tsx
|
||||
export const App = () => {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Static Server-side-rendered App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>Hello World!</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// server.tsx
|
||||
import express from 'express'
|
||||
import React from 'react'
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
|
||||
import { App } from './app.tsx'
|
||||
|
||||
const port = Number.parseInt(process.env.PORT || '3000', 10)
|
||||
const app = express()
|
||||
|
||||
app.get('/', (_, res) => {
|
||||
const { pipe } = ReactDOMServer.renderToPipeableStream(<App />, {
|
||||
onShellReady() {
|
||||
res.setHeader('content-type', 'text/html')
|
||||
pipe(res)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is listening at ${port}`)
|
||||
})
|
||||
```
|
||||
|
||||
```sh
|
||||
tsc --build
|
||||
```
|
||||
|
||||
```sh
|
||||
node server.js
|
||||
```
|
||||
|
||||
## Hydration
|
||||
|
||||
Hydration turns the initial HTML snapshot from the server into a fully interactive app
|
||||
that runs in the browser. The right way to "hydrate" a component is by using `hydrateRoot`.
|
||||
|
||||
### React
|
||||
|
||||
Let's say we want to render an statefull app using react. In order to do that we need to
|
||||
use `express`, `react`, `react-dom/server` and `react-dom/client`.
|
||||
|
||||
Let's dive into that:
|
||||
|
||||
- `express` helps us build a web app that we can run using Node,
|
||||
- `react` helps us build the UI components that we use in our app,
|
||||
- `react-dom/server` helps us render our components on a server,
|
||||
- `react-dom/client` helps us hydrate our components on a client.
|
||||
|
||||
> **Note:** Do not forget that even if we can render our components on a server, it is
|
||||
> important to "hydrate" them on a client to make them interactive.
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"noEmitOnError": true,
|
||||
"removeComments": false,
|
||||
"sourceMap": true,
|
||||
"target": "esnext"
|
||||
},
|
||||
"include": ["**/*"]
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** do not forget to remove all comments in your `tsconfig.json` file.
|
||||
|
||||
```tsx
|
||||
// app.tsx
|
||||
export const App = () => {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Static Server-side-rendered App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>Hello World!</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// main.tsx
|
||||
import ReactDOMClient from 'react-dom/client'
|
||||
|
||||
import { App } from './app.tsx'
|
||||
|
||||
ReactDOMClient.hydrateRoot(<App />, document)
|
||||
```
|
||||
|
||||
```tsx
|
||||
// server.tsx
|
||||
import express from 'express'
|
||||
import React from 'react'
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
|
||||
import { App } from './app.tsx'
|
||||
|
||||
const port = Number.parseInt(process.env.PORT || '3000', 10)
|
||||
const app = express()
|
||||
|
||||
app.use('/', (_, res) => {
|
||||
const { pipe } = ReactDOMServer.renderToPipeableStream(<App />, {
|
||||
bootstrapScripts: ['/main.js'],
|
||||
onShellReady() {
|
||||
res.setHeader('content-type', 'text/html')
|
||||
pipe(res)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is listening at ${port}`)
|
||||
})
|
||||
```
|
||||
|
||||
```sh
|
||||
tsc --build
|
||||
```
|
||||
|
||||
```sh
|
||||
node server.js
|
||||
```
|
||||
|
||||
> **Warning:** The React tree you pass to `hydrateRoot` needs to produce the same output as it did on the server.
|
||||
> The most common causes leading to hydration errors include:
|
||||
>
|
||||
> - Extra whitespace (like newlines) around the React-generated HTML inside the root node.
|
||||
> - Using checks like typeof window !== 'undefined' in your rendering logic.
|
||||
> - Using browser-only APIs like `window.matchMedia` in your rendering logic.
|
||||
> - Rendering different data on the server and the client.
|
||||
>
|
||||
> React recovers from some hydration errors, but you must fix them like other bugs. In the best case, they’ll lead to a slowdown; in the worst case, event handlers can get attached to the wrong elements.
|
||||
|
||||
You can read more about the caveats and pitfalls here: [hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot)
|
||||
@ -79,7 +79,7 @@ export const storeResetFns = new Set<() => void>()
|
||||
|
||||
const createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
const store = actualCreate(stateCreator)
|
||||
const initialState = store.getState()
|
||||
const initialState = store.getInitialState()
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true)
|
||||
})
|
||||
@ -98,7 +98,7 @@ export const create = (<T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
|
||||
const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
const store = actualCreateStore(stateCreator)
|
||||
const initialState = store.getState()
|
||||
const initialState = store.getInitialState()
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true)
|
||||
})
|
||||
@ -168,7 +168,7 @@ export const storeResetFns = new Set<() => void>()
|
||||
|
||||
const createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
const store = actualCreate(stateCreator)
|
||||
const initialState = store.getState()
|
||||
const initialState = store.getInitialState()
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true)
|
||||
})
|
||||
@ -187,7 +187,7 @@ export const create = (<T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
|
||||
const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
const store = actualCreateStore(stateCreator)
|
||||
const initialState = store.getState()
|
||||
const initialState = store.getInitialState()
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true)
|
||||
})
|
||||
|
||||
@ -259,7 +259,11 @@ const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => {
|
||||
set(...a)
|
||||
console.log(...(name ? [`${name}:`] : []), get())
|
||||
}
|
||||
store.setState = loggedSet
|
||||
const setState = store.setState
|
||||
store.setState = (...a) => {
|
||||
setState(...a)
|
||||
console.log(...(name ? [`${name}:`] : []), store.getState())
|
||||
}
|
||||
|
||||
return f(loggedSet, get, store)
|
||||
}
|
||||
|
||||
30
package.json
30
package.json
@ -108,29 +108,29 @@
|
||||
},
|
||||
"homepage": "https://github.com/pmndrs/zustand",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.7",
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/plugin-external-helpers": "^7.23.3",
|
||||
"@babel/plugin-transform-react-jsx": "^7.23.4",
|
||||
"@babel/plugin-transform-runtime": "^7.23.7",
|
||||
"@babel/plugin-transform-runtime": "^7.23.9",
|
||||
"@babel/plugin-transform-typescript": "^7.23.6",
|
||||
"@babel/preset-env": "^7.23.8",
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"@redux-devtools/extension": "^3.3.0",
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@types/node": "^20.11.5",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@testing-library/react": "^14.2.1",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||
"@typescript-eslint/parser": "^6.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||
"@typescript-eslint/parser": "^7.0.1",
|
||||
"@vitest/coverage-v8": "0.33.0",
|
||||
"@vitest/ui": "0.33.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"esbuild": "^0.19.11",
|
||||
"esbuild": "^0.20.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
@ -138,16 +138,16 @@
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-vitest": "^0.3.20",
|
||||
"eslint-plugin-vitest": "^0.3.22",
|
||||
"immer": "^10.0.3",
|
||||
"jsdom": "^23.2.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"json": "^11.0.0",
|
||||
"prettier": "^3.2.4",
|
||||
"prettier": "^3.2.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"redux": "^5.0.1",
|
||||
"rollup": "^4.9.5",
|
||||
"rollup-plugin-esbuild": "^6.1.0",
|
||||
"rollup": "^4.12.0",
|
||||
"rollup-plugin-esbuild": "^6.1.1",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.3.3",
|
||||
"use-sync-external-store": "^1.2.0",
|
||||
|
||||
25
readme.md
25
readme.md
@ -284,31 +284,6 @@ clearForest()
|
||||
|
||||
[Alternatively, there are some other solutions.](./docs/guides/updating-state.md#with-immer)
|
||||
|
||||
## Middleware
|
||||
|
||||
You can functionally compose your store any way you like.
|
||||
|
||||
```jsx
|
||||
// Log every time state is changed
|
||||
const log = (config) => (set, get, api) =>
|
||||
config(
|
||||
(...args) => {
|
||||
console.log(' applying', args)
|
||||
set(...args)
|
||||
console.log(' new state', get())
|
||||
},
|
||||
get,
|
||||
api,
|
||||
)
|
||||
|
||||
const useBeeStore = create(
|
||||
log((set) => ({
|
||||
bees: false,
|
||||
setBees: (input) => set({ bees: input }),
|
||||
})),
|
||||
)
|
||||
```
|
||||
|
||||
## Persist middleware
|
||||
|
||||
You can persist your store's data using any kind of storage.
|
||||
|
||||
@ -6,8 +6,8 @@ import type {
|
||||
|
||||
export interface StateStorage {
|
||||
getItem: (name: string) => string | null | Promise<string | null>
|
||||
setItem: (name: string, value: string) => void | Promise<void>
|
||||
removeItem: (name: string) => void | Promise<void>
|
||||
setItem: (name: string, value: string) => unknown | Promise<unknown>
|
||||
removeItem: (name: string) => unknown | Promise<unknown>
|
||||
}
|
||||
|
||||
export type StorageValue<S> = {
|
||||
@ -19,8 +19,8 @@ export interface PersistStorage<S> {
|
||||
getItem: (
|
||||
name: string,
|
||||
) => StorageValue<S> | null | Promise<StorageValue<S> | null>
|
||||
setItem: (name: string, value: StorageValue<S>) => void | Promise<void>
|
||||
removeItem: (name: string) => void | Promise<void>
|
||||
setItem: (name: string, value: StorageValue<S>) => unknown | Promise<unknown>
|
||||
removeItem: (name: string) => unknown | Promise<unknown>
|
||||
}
|
||||
|
||||
type JsonStorageOptions = {
|
||||
@ -200,7 +200,7 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
|
||||
)
|
||||
}
|
||||
|
||||
const setItem = (): void | Promise<void> => {
|
||||
const setItem = () => {
|
||||
const state = options.partialize({ ...get() })
|
||||
return (storage as PersistStorage<S>).setItem(options.name, {
|
||||
state,
|
||||
|
||||
@ -14,9 +14,9 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"zustand": ["./src/index.ts"],
|
||||
"zustand/*": ["./src/*.ts"],
|
||||
},
|
||||
"zustand/*": ["./src/*.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "tests/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user