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.0.0
|
||||||
- 18.1.0
|
- 18.1.0
|
||||||
- 18.2.0
|
- 18.2.0
|
||||||
- 18.3.0-canary-feed8f3f9-20240118
|
- 18.3.0-canary-a9cc32511-20240215
|
||||||
- 0.0.0-experimental-feed8f3f9-20240118
|
- 0.0.0-experimental-a9cc32511-20240215
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@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 createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||||
const store = actualCreate(stateCreator)
|
const store = actualCreate(stateCreator)
|
||||||
const initialState = store.getState()
|
const initialState = store.getInitialState()
|
||||||
storeResetFns.add(() => {
|
storeResetFns.add(() => {
|
||||||
store.setState(initialState, true)
|
store.setState(initialState, true)
|
||||||
})
|
})
|
||||||
@ -98,7 +98,7 @@ export const create = (<T>(stateCreator: zustand.StateCreator<T>) => {
|
|||||||
|
|
||||||
const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||||
const store = actualCreateStore(stateCreator)
|
const store = actualCreateStore(stateCreator)
|
||||||
const initialState = store.getState()
|
const initialState = store.getInitialState()
|
||||||
storeResetFns.add(() => {
|
storeResetFns.add(() => {
|
||||||
store.setState(initialState, true)
|
store.setState(initialState, true)
|
||||||
})
|
})
|
||||||
@ -168,7 +168,7 @@ export const storeResetFns = new Set<() => void>()
|
|||||||
|
|
||||||
const createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
const createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||||
const store = actualCreate(stateCreator)
|
const store = actualCreate(stateCreator)
|
||||||
const initialState = store.getState()
|
const initialState = store.getInitialState()
|
||||||
storeResetFns.add(() => {
|
storeResetFns.add(() => {
|
||||||
store.setState(initialState, true)
|
store.setState(initialState, true)
|
||||||
})
|
})
|
||||||
@ -187,7 +187,7 @@ export const create = (<T>(stateCreator: zustand.StateCreator<T>) => {
|
|||||||
|
|
||||||
const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||||
const store = actualCreateStore(stateCreator)
|
const store = actualCreateStore(stateCreator)
|
||||||
const initialState = store.getState()
|
const initialState = store.getInitialState()
|
||||||
storeResetFns.add(() => {
|
storeResetFns.add(() => {
|
||||||
store.setState(initialState, true)
|
store.setState(initialState, true)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -259,7 +259,11 @@ const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => {
|
|||||||
set(...a)
|
set(...a)
|
||||||
console.log(...(name ? [`${name}:`] : []), get())
|
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)
|
return f(loggedSet, get, store)
|
||||||
}
|
}
|
||||||
|
|||||||
30
package.json
30
package.json
@ -108,29 +108,29 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/pmndrs/zustand",
|
"homepage": "https://github.com/pmndrs/zustand",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.23.7",
|
"@babel/core": "^7.23.9",
|
||||||
"@babel/plugin-external-helpers": "^7.23.3",
|
"@babel/plugin-external-helpers": "^7.23.3",
|
||||||
"@babel/plugin-transform-react-jsx": "^7.23.4",
|
"@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/plugin-transform-typescript": "^7.23.6",
|
||||||
"@babel/preset-env": "^7.23.8",
|
"@babel/preset-env": "^7.23.9",
|
||||||
"@redux-devtools/extension": "^3.3.0",
|
"@redux-devtools/extension": "^3.3.0",
|
||||||
"@rollup/plugin-alias": "^5.1.0",
|
"@rollup/plugin-alias": "^5.1.0",
|
||||||
"@rollup/plugin-babel": "^6.0.4",
|
"@rollup/plugin-babel": "^6.0.4",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-replace": "^5.0.5",
|
"@rollup/plugin-replace": "^5.0.5",
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
"@testing-library/react": "^14.1.2",
|
"@testing-library/react": "^14.2.1",
|
||||||
"@types/node": "^20.11.5",
|
"@types/node": "^20.11.19",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.55",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.19",
|
||||||
"@types/use-sync-external-store": "^0.0.6",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||||
"@typescript-eslint/parser": "^6.19.0",
|
"@typescript-eslint/parser": "^7.0.1",
|
||||||
"@vitest/coverage-v8": "0.33.0",
|
"@vitest/coverage-v8": "0.33.0",
|
||||||
"@vitest/ui": "0.33.0",
|
"@vitest/ui": "0.33.0",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"esbuild": "^0.19.11",
|
"esbuild": "^0.20.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
@ -138,16 +138,16 @@
|
|||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-vitest": "^0.3.20",
|
"eslint-plugin-vitest": "^0.3.22",
|
||||||
"immer": "^10.0.3",
|
"immer": "^10.0.3",
|
||||||
"jsdom": "^23.2.0",
|
"jsdom": "^24.0.0",
|
||||||
"json": "^11.0.0",
|
"json": "^11.0.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"rollup": "^4.9.5",
|
"rollup": "^4.12.0",
|
||||||
"rollup-plugin-esbuild": "^6.1.0",
|
"rollup-plugin-esbuild": "^6.1.1",
|
||||||
"shx": "^0.3.4",
|
"shx": "^0.3.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"use-sync-external-store": "^1.2.0",
|
"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)
|
[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
|
## Persist middleware
|
||||||
|
|
||||||
You can persist your store's data using any kind of storage.
|
You can persist your store's data using any kind of storage.
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import type {
|
|||||||
|
|
||||||
export interface StateStorage {
|
export interface StateStorage {
|
||||||
getItem: (name: string) => string | null | Promise<string | null>
|
getItem: (name: string) => string | null | Promise<string | null>
|
||||||
setItem: (name: string, value: string) => void | Promise<void>
|
setItem: (name: string, value: string) => unknown | Promise<unknown>
|
||||||
removeItem: (name: string) => void | Promise<void>
|
removeItem: (name: string) => unknown | Promise<unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StorageValue<S> = {
|
export type StorageValue<S> = {
|
||||||
@ -19,8 +19,8 @@ export interface PersistStorage<S> {
|
|||||||
getItem: (
|
getItem: (
|
||||||
name: string,
|
name: string,
|
||||||
) => StorageValue<S> | null | Promise<StorageValue<S> | null>
|
) => StorageValue<S> | null | Promise<StorageValue<S> | null>
|
||||||
setItem: (name: string, value: StorageValue<S>) => void | Promise<void>
|
setItem: (name: string, value: StorageValue<S>) => unknown | Promise<unknown>
|
||||||
removeItem: (name: string) => void | Promise<void>
|
removeItem: (name: string) => unknown | Promise<unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
type JsonStorageOptions = {
|
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() })
|
const state = options.partialize({ ...get() })
|
||||||
return (storage as PersistStorage<S>).setItem(options.name, {
|
return (storage as PersistStorage<S>).setItem(options.name, {
|
||||||
state,
|
state,
|
||||||
|
|||||||
@ -14,9 +14,9 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"zustand": ["./src/index.ts"],
|
"zustand": ["./src/index.ts"],
|
||||||
"zustand/*": ["./src/*.ts"],
|
"zustand/*": ["./src/*.ts"]
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "tests/**/*"],
|
"include": ["src/**/*", "tests/**/*"],
|
||||||
"exclude": ["node_modules", "dist"],
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user