Skip to content

Latest commit

 

History

History
492 lines (360 loc) · 17 KB

README.md

File metadata and controls

492 lines (360 loc) · 17 KB

Zustand Cover

Build Status Build Size Version Downloads Discord Shield

一个小巧、快速且可扩展的基础状态管理解决方案,使用简化的 flux 原则。它基于 hooks 的 API 使用起来很舒服,没有样板代码,也不带有任何观点。

不要因为它看起来很可爱就忽视它。它有相当锐利的爪子,我们花了很多时间来处理常见的陷阱,比如令人头疼的僵尸子问题react 并发,以及在混合渲染器之间的上下文丢失。它可能是 React 领域中唯一一个能正确处理所有这些问题的状态管理器。

你可以在这里试用一个在线演示。

npm install zustand # 或者 yarn add zustand 或者 pnpm add zustand

⚠️ 这个 readme 是为 JavaScript 用户编写的。如果你是 TypeScript 用户,一定要查看我们的 TypeScript 使用部分

首先创建一个存储

你的存储就是一个 hook!你可以在其中放置任何东西:原始值、对象、函数。状态必须以不可变的方式更新,set 函数合并状态以帮助实现这一点。

import { create } from 'zustand';

const useBearStore = create(set => ({
    bears: 0,
    increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
    removeAllBears: () => set({ bears: 0 }),
}));

然后绑定你的组件,就这样!

在任何地方使用这个 hook,不需要提供者。选择你的状态,当状态发生变化时,组件将重新渲染。

function BearCounter() {
    const bears = useBearStore(state => state.bears);
    return <h1>{bears} 在这里 ...</h1>;
}

function Controls() {
    const increasePopulation = useBearStore(state => state.increasePopulation);
    return <button onClick={increasePopulation}>增加一只</button>;
}

为什么选择 zustand 而不是 redux?

为什么选择 zustand 而不是 context?

  • 更少的样板代码
  • 只在状态变化时渲染组件
  • 集中的,基于操作的状态管理

食谱

获取所有内容

你可以这样做,但请记住,这将导致组件在每次状态变化时都更新!

const state = useBearStore();

选择多个状态切片

默认情况下,它使用严格相等(old === new)来检测变化,这对于原子状态选择非常高效。

const nuts = useBearStore(state => state.nuts);
const honey = useBearStore(state => state.honey);

如果你想构造一个包含多个状态选择的单一对象,类似于 redux 的 mapStateToProps,你可以使用 useShallow 来防止选择器输出不变时的不必要重新渲染。

import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';

const useBearStore = create(set => ({
    bears: 0,
    increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
    removeAllBears: () => set({ bears: 0 }),
}));

// 对象选择,当 state.nuts 或 state.honey 变化时,重新渲染组件
const { nuts, honey } = useBearStore(useShallow(state => ({ nuts: state.nuts, honey: state.honey })));

// 数组选择,当 state.nuts 或 state.honey 变化时,重新渲染组件
const [nuts, honey] = useBearStore(useShallow(state => [state.nuts, state.honey]));

// 映射选择,当 state.treats 的顺序、数量或键发生变化时,重新渲染组件
const treats = useBearStore(useShallow(state => Object.keys(state.treats)));

为了更好地控制重新渲染,你可以提供任何自定义的相等函数。

const treats = useBearStore(
    state => state.treats,
    (oldTreats, newTreats) => compare(oldTreats, newTreats)
);

覆盖状态

set 函数有一个第二个参数,默认为 false。它不会合并,而是替换状态模型。小心不要擦掉你依赖的部分,比如操作。

import omit from 'lodash-es/omit';

const useFishStore = create(set => ({
    salmon: 1,
    tuna: 2,
    deleteEverything: () => set({}, true), // 清除整个存储,包括操作
    deleteTuna: () => set(state => omit(state, ['tuna']), true),
}));

异步操作

只需在准备好时调用 set,zustand 不关心你的操作是异步还是同步的。

const useFishStore = create(set => ({
    fishies: {},
    fetch: async pond => {
        const response = await fetch(pond);
        set({ fishies: await response.json() });
    },
}));

在操作中读取状态

set 允许函数更新 set(state => result),但你仍然可以通过 get 在其外部访问状态。

const useSoundStore = create((set, get) => ({
  sound: 'grunt',
  action: () => {
    const sound = get().sound
    ...

在组件外部读写状态并对变化做出反应

有时你需要以非响应式的方式访问状态或对存储进行操作。对于这些情况,结果钩子在其原型上附加了实用函数。

⚠️ 这种技术不推荐在 React 服务器组件 中添加状态(通常在 Next.js 13 及以上版本中)。它可能会导致意外的错误和用户隐私问题。更多详情,请参见 #2200

const useDogStore = create(() => ({ paw: true, snout: true, fur: true }))

// 获取非响应式的新状态
const paw = useDogStore.getState().paw
// 监听所有变化,每次变化时同步触发
const unsub1 = useDogStore.subscribe(console.log)
// 更新状态,将触发监听器
useDogStore.setState({ paw: false })
// 取消订阅监听器
unsub1()

// 你当然可以像以前一样使用钩子
function Component() {
  const paw = useDogStore((state) => state.paw)
  ...

使用选择器订阅

如果你需要使用选择器订阅,subscribeWithSelector 中间件将会有所帮助。

使用这个中间件,subscribe 接受一个额外的签名:

subscribe(selector, callback, options?: { equalityFn, fireImmediately }): Unsubscribe
import { subscribeWithSelector } from 'zustand/middleware';
const useDogStore = create(subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })));

// 监听选定的变化,在这种情况下,当 "paw" 变化时
const unsub2 = useDogStore.subscribe(state => state.paw, console.log);
// Subscribe 还暴露了前一个值
const unsub3 = useDogStore.subscribe(
    state => state.paw,
    (paw, previousPaw) => console.log(paw, previousPaw)
);
// Subscribe 还支持一个可选的等式函数
const unsub4 = useDogStore.subscribe(state => [state.paw, state.fur], console.log, { equalityFn: shallow });
// 立即订阅并触发
const unsub5 = useDogStore.subscribe(state => state.paw, console.log, {
    fireImmediately: true,
});

不使用 React 使用 zustand

Zustand 核心可以导入并在没有 React 依赖的情况下使用。唯一的区别是 create 函数不返回钩子,而是返回 API 实用程序。

import { createStore } from 'zustand/vanilla'

const store = createStore((set) => ...)
const { getState, setState, subscribe, getInitialState } = store

export default store

你可以使用 useStore 钩子与 vanilla store 一起使用,这个钩子自 v4 版本以来就可用。

import { useStore } from 'zustand';
import { vanillaStore } from './vanillaStore';

const useBoundStore = selector => useStore(vanillaStore, selector);

⚠️ 注意,修改 setget 的中间件不会应用于 getStatesetState

瞬态更新(用于经常发生的状态变化)

subscribe 函数允许组件绑定到一个状态部分,而不强制在变化时重新渲染。最好与 useEffect 结合使用,以在卸载时自动取消订阅。当你被允许直接修改视图时,这可以产生显著的性能影响。

const useScratchStore = create((set) => ({ scratches: 0, ... }))

const Component = () => {
  // 获取初始状态
  const scratchRef = useRef(useScratchStore.getState().scratches)
  // 在挂载时连接到存储,在卸载时断开连接,在引用中捕获状态变化
  useEffect(() => useScratchStore.subscribe(
    state => (scratchRef.current = state.scratches)
  ), [])
  ...

厌倦了 reducers 和改变嵌套状态?试试 Immer!

减少嵌套结构是令人疲倦的。你试过 immer 吗?

import { produce } from 'immer';

const useLushStore = create(set => ({
    lush: { forest: { contains: { a: 'bear' } } },
    clearForest: () =>
        set(
            produce(state => {
                state.lush.forest.contains = null;
            })
        ),
}));

const clearForest = useLushStore(state => state.clearForest);
clearForest();

另外,还有一些其他的解决方案。

持久化中间件

你可以使用任何类型的存储来持久化你的存储数据。

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

const useFishStore = create(
    persist(
        (set, get) => ({
            fishes: 0,
            addAFish: () => set({ fishes: get().fishes + 1 }),
        }),
        {
            name: 'food-storage', // 存储项的名称(必须唯一)
            storage: createJSONStorage(() => sessionStorage), // (可选)默认情况下,使用 'localStorage'
        }
    )
);

查看此中间件的完整文档。

Immer 中间件

Immer 也可以作为中间件使用。

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const useBeeStore = create(
    immer(set => ({
        bees: 0,
        addBees: by =>
            set(state => {
                state.bees += by;
            }),
    }))
);

离不开类似Redux的reducers和action类型吗?

const types = { increase: 'INCREASE', decrease: 'DECREASE' };

const reducer = (state, { type, by = 1 }) => {
    switch (type) {
        case types.increase:
            return { grumpiness: state.grumpiness + by };
        case types.decrease:
            return { grumpiness: state.grumpiness - by };
    }
};

const useGrumpyStore = create(set => ({
    grumpiness: 0,
    dispatch: args => set(state => reducer(state, args)),
}));

const dispatch = useGrumpyStore(state => state.dispatch);
dispatch({ type: types.increase, by: 2 });

或者,只需使用我们的 redux-middleware。它将连接你的主 reducer,设置初始状态,并将 dispatch 函数添加到状态本身和 vanilla API。

import { redux } from 'zustand/middleware';

const useGrumpyStore = create(redux(reducer, initialState));

Redux devtools

import { devtools } from 'zustand/middleware'

// 与普通 action store 的使用,它将记录 actions 为 "setState"
const usePlainStore = create(devtools((set) => ...))
// 与 redux store 的使用,它将记录完整的 action types
const useReduxStore = create(devtools(redux(reducer, initialState)))

一个 redux devtools 连接多个 stores

import { devtools } from 'zustand/middleware'

// 与普通 action store 的使用,它将记录 actions 为 "setState"
const usePlainStore1 = create(devtools((set) => ..., { name, store: storeName1 }))
const usePlainStore2 = create(devtools((set) => ..., { name, store: storeName2 }))
// 与 redux store 的使用,它将记录完整的 action types
const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName3 })
const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName4 })

分配不同的连接名称将在 redux devtools 中分隔 stores。这也有助于将不同的 stores 分组到单独的 redux devtools 连接中。

devtools 将 store 函数作为其第一个参数,你可以选择性地为 store 命名或使用第二个参数配置 serialize 选项。

命名 store:devtools(..., {name: "MyStore"}),这将在 devtools 中创建一个名为 "MyStore" 的单独实例。

序列化选项:devtools(..., { serialize: { options: true } })

记录操作

devtools 只会记录每个独立存储的操作,这与典型的 combined reducers redux 存储不同。关于如何合并存储的方法,可以参考 pmndrs/zustand#163

你可以通过传递第三个参数为每个 set 函数记录特定的操作类型:

const useBearStore = create(devtools((set) => ({
    ...
    eatFish: () => set(
        (prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 }),
        false,
        'bear/eatFish'
    ),
    ...

你也可以记录操作的类型以及其负载:

    ...
    addFishes: (count) => set(
        (prev) => ({ fishes: prev.fishes + count }),
        false,
        { type: 'bear/addFishes', count, }
    ),
    ...

如果没有提供操作类型,它将默认为 "anonymous"。你可以通过提供 anonymousActionType 参数来自定义此默认值:

devtools(..., { anonymousActionType: 'unknown', ... })

如果你希望禁用 devtools(例如在生产环境中)。你可以通过提供 enabled 参数来自定义此设置:

devtools(..., { enabled: false, ... })

React 上下文

使用 create 创建的存储不需要上下文提供者。在某些情况下,你可能希望使用上下文进行依赖注入,或者如果你想用组件的 props 初始化你的存储。因为普通的存储是一个钩子,将它作为一个普通的上下文值可能会违反钩子的规则。

自 v4 版本开始,推荐的方法是使用 vanilla 存储。

import { createContext, useContext } from 'react'
import { createStore, useStore } from 'zustand'

const store = createStore(...) // 不带钩子的 vanilla 存储

const StoreContext = createContext()

const App = () => (
    <StoreContext.Provider value={store}>
        ...
    </StoreContext.Provider>
)

const Component = () => {
    const store = useContext(StoreContext)
    const slice = useStore(store, selector)
    ...

TypeScript 使用

基本的 TypeScript 使用不需要任何特殊的东西,只需要写 create<State>()(...) 而不是 create(...)...

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import type {} from '@redux-devtools/extension'; // 需要 devtools 类型

interface BearState {
    bears: number;
    increase: (by: number) => void;
}

const useBearStore = create<BearState>()(
    devtools(
        persist(
            set => ({
                bears: 0,
                increase: by => set(state => ({ bears: state.bears + by })),
            }),
            {
                name: 'bear-storage',
            }
        )
    )
);

更完整的 TypeScript 指南在这里

最佳实践

第三方库

一些用户可能希望扩展 Zustand 的功能集,这可以通过使用社区制作的第三方库来完成。有关 Zustand 的第三方库的信息,请访问文档

与其他库的比较