A small, fast, and scalable bearbones state management solution. Zustand has a comfy API based on hooks. It isn’t boilerplatey or opinionated, but has enough convention to be explicit and flux-like.

Zustand

Demo

这里讲的是 V5 版本。

核心特性与场景

1 状态获取与更新

默认采用合并更新:Zustand 的 set 函数默认会合并新状态到原状态。

替换更新:若需完全替换状态,需设置第二个参数 replace: true

set((state) => newState, true) // 完全替换

深度嵌套对象的更新

set((state) => ({
    ...state,
    nested: {
      ...state.nested,
      deepProp: newValue
    }
  }))

可以用 immer 来优化:

set(produce(state => state.nested.deepProp = newValue))

多值获取:通过 useStoreuseShallow 组合获取多个值:

const [searchValue, setSearchValue] = useStore(
  useShallow((state) => [state.searchValue, state.setSearchValue])
)

2 多实例管理

Zustand 的 Store 是全局的,只有一个实例。要实现多实例,可以用工厂来创建实例:

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 })),
  }))
}

然后在存到 React 的 Context 中。

export const CounterStoreProvider = ({
  children,
}: CounterStoreProviderProps) => {
  const storeRef = useRef<CounterStoreApi | null>(null)
  if (storeRef.current === null) {
    storeRef.current = createCounterStore()
  }
 
  return (
    <CounterStoreContext.Provider value={storeRef.current}>
      {children}
    </CounterStoreContext.Provider>
  )
}

3 状态持久化

persist 可以默认持久化到 localStorage。也可以持久化到 url 的 hash 的其他地方,只要实现了方法: getItemsetItemremoveItem。如:

import { create } from 'zustand'
import { persist, StateStorage, createJSONStorage } from 'zustand/middleware'
 
const hashStorage: StateStorage = {
  getItem: (key): string => {
    const searchParams = new URLSearchParams(location.hash.slice(1))
    const storedValue = searchParams.get(key) ?? ''
    return JSON.parse(storedValue)
  },
  setItem: (key, newValue): void => {
    const searchParams = new URLSearchParams(location.hash.slice(1))
    searchParams.set(key, JSON.stringify(newValue))
    location.hash = searchParams.toString()
  },
  removeItem: (key): void => {
    const searchParams = new URLSearchParams(location.hash.slice(1))
    searchParams.delete(key)
    location.hash = searchParams.toString()
  },
}
 
export const useBoundStore = create(
  persist(
    (set, get) => ({
      fishes: 0,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
    {
      name: 'food-storage', // unique name
      storage: createJSONStorage(() => hashStorage),
    },
  ),
)

4 切片模式(Slices Pattern)

拆分大型 Store:每个功能模块独立管理状态:

// fishSlice.js
export const createFishSlice = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
});
 
// bearSlice.js
export const createBearSlice = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
});
 
// 合并 Store
const useCombinedStore = create(() => ({
  ...createFishSlice(set),
  ...createBearSlice(set),
}));

性能优化

避免不必要的渲染。通过 selector 的方式来取值。

const firstName = usePersonStore((state) => state.firstName)

不用选择器选若干个属性,要避免不必要的渲染,可以用 useShallow。比如:

// store
const useMeals = create(() => ({
  papaBear: 'large porridge-pot',
  mamaBear: 'middle-size porridge pot',
  littleBear: 'A little, small, wee pot',
}))
 
export const BearNames = () => {
  const names = useMeals((state) => Object.keys(state))
 
  return <div>{names.join(', ')}</div>
}

如果 store 的 key 没有发生变化,但 value 发生变化,还是会重新渲染。

useMeals.setState({
  papaBear: 'a large pizza',
})

useShallow 可以避免这种情况:

const names = useMeals(useShallow((state) => Object.keys(state)))

其他

自动生成选择器。封装后,api 变成:

// get the property
const bears = useBearStore.use.bears()
// 不这么处理的写法: const bears = useBearStore(state => state.bears)
 
// get the action
const increment = useBearStore.use.increment()

测试。Mock Zustand。

v4 到 v5 的变动