什么是zustand
zustand是一种小型快的的状态管理框架,拥有基于hook的舒适小巧的API,对比Mobx和Redux,它更小巧,学习曲线更平稳。
git地址:github.com/pmndrs/zust…
基础用法
1.创建store
import { create } from 'zustand'
//数据State类型
export interface TestState {
data: number; //数据类型
asyncIncrease: (by: number) => Promise<void>; //修改数据的异步方法
increase: (by: number) => void; //修改数据的同步方法
removeAll: () => void;
}
//创建返回数据对象的hook
export const useTestState = create<TestState>()((set, getState, store) => ({
data: 1,
asyncIncrease: async (by: number) => {
await set((state) => ({ data: state.data + by }))
},
increase: (by) => set((state) => ({ data: state.data + by })),
removeAll: () => set({ data: 0 })
}));
先创建一个State类型进行声明,然后调用zustand的create方法创建一个hook方法,该方法可以返回state对象和修改state对象的action(incrase,asyncIncrease等方法),其中create()传入一个函数,该函数类型接收三个参数并且返回一个泛型定义的State对象(TestState),第一个参数set也是一个函数,可以接收state对象或者一个入参为state,返回值为state的函数,在这里可以对state进行修改,然后触发组件的重新渲染。注意这里采用的是属性合并的模式,所以set调用里只需要对有变化的属性修改返回即可。
2.组件使用
import React, { type VFC, memo } from 'react';
import {Text, TouchableOpacity, View } from 'react-native';
import { type AppInitialProps } from '@mfe/plus';
import { useStyles } from './styles';
import { useTestState } from '@/store/appStore';
const App: VFC<AppInitialProps> = () => {
//调用state hook方法
const { data, increase, removeAll } = useTestState();
// const increase = useTestState((state) => state.increase);
const styles = useStyles();
return (
<View style={styles.container}>
<TouchableOpacity onPress={() => { removeAll() }}> //调用state进行方法更新。
<Text>{`data的数量是${data}`}</Text> //引用state
</TouchableOpacity>
</View>
);
};
export default memo<typeof App>(App);
以上就是完整的zustand的使用方法,非常简洁,不需要useState,不需要useEffect,也不需要进行相应的依赖关心。
3.源码分析
useSyncExternalStore分析
先介绍一下一个在react 18新增的一个hook api
export function useSyncExternalStore<Snapshot>(
subscribe: (onStoreChange: () => void) => () => void,
getSnapshot: () => Snapshot,
getServerSnapshot?: () => Snapshot,
): Snapshot;
它的作用就是允许你订阅一个外部存储State,并且在外部存储State更新时,触发组件的刷新。它的意义是什么?举个例子
import React from 'react';
import { Text, View,TouchableOpacity } from 'react-native';
const store = {
currentState:{data:0},
listeners:[],
reducer(action){
switch(action.type) {
case 'ADD':
return {data:store.currentState.data+1}
default:
return store.state
}
},
subscribe(l){
store.listeners.push(l)
},
getSnapshot() {
return store.currentState
},
dispatch(action) {
store.currentState = store.reducer(action)
store.listeners.forEach(l=>l())
return action;
}
}
const YourApp: VFC<AppInitialProps> = () => {
const outerState = React.useSyncExternalStore(store.subscribe, ()=>store.getSnapshot().data);
console.log(`outState is ${outerState}`);
return (
<View style={{flex:1}}>
<TouchableOpacity onPress={() => {
store.dispatch({type:'ADD'})}}>
<Text>{`data的数量是${JSON.stringify(outerState)}`}</Text>
</TouchableOpacity>
</View>
);
};
export default YourApp;
我自己定义了一个store,它内部进行自己状态的管理,并且定义了一系列行为,我在组件内部使用的时候,只需要使用useSyncExternalStore将对store里暴露的两个方法关联进去即可,后续我们直接使用outerState里面的数据和行为进行展示和刷新。
通过上面的代码你会发现,页面的刷新除了useSyncExternalStore其实你并没有使用类似useState和useEffect等hook,这就是它的价值所在,他为我们提供了一种store的管理维护方法,并且不用使用任何内置hook。
看一下useSyncExternalStore的源码
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import is from 'shared/objectIs';
// Intentionally not using named imports because Rollup uses dynamic
// dispatch for CommonJS interop named imports.
const {useState, useEffect, useLayoutEffect, useDebugValue} = React;
let didWarnOld18Alpha = false;
let didWarnUncachedGetSnapshot = false;
// Disclaimer: This shim breaks many of the rules of React, and only works
// because of a very particular set of implementation details and assumptions
// -- change any one of them and it will break. The most important assumption
// is that updates are always synchronous, because concurrent rendering is
// only available in versions of React that also have a built-in
// useSyncExternalStore API. And we only use this shim when the built-in API
// does not exist.
//
// Do not assume that the clever hacks used by this hook also work in general.
// The point of this shim is to replace the need for hacks by other libraries.
export function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
// Note: The shim does not use getServerSnapshot, because pre-18 versions of
// React do not expose a way to check if we're hydrating. So users of the shim
// will need to track that themselves and return the correct value
// from `getSnapshot`.
getServerSnapshot?: () => T,
): T {
// Read the current snapshot from the store on every render. Again, this
// breaks the rules of React, and only works here because of specific
// implementation details, most importantly that updates are
// always synchronous.
const value = getSnapshot();
// Because updates are synchronous, we don't queue them. Instead we force a
// re-render whenever the subscribed state changes by updating an some
// arbitrary useState hook. Then, during render, we call getSnapshot to read
// the current value.
//
// Because we don't actually use the state returned by the useState hook, we
// can save a bit of memory by storing other stuff in that slot.
//
// To implement the early bailout, we need to track some things on a mutable
// object. Usually, we would put that in a useRef hook, but we can stash it in
// our useState hook instead.
//
// To force a re-render, we call forceUpdate({inst}). That works because the
// new object always fails an equality check.
const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});
// Track the latest getSnapshot function with a ref. This needs to be updated
// in the layout phase so we can access it during the tearing check that
// happens on subscribe.
useLayoutEffect(() => {
inst.value = value;
inst.getSnapshot = getSnapshot;
// Whenever getSnapshot or subscribe changes, we need to check in the
// commit phase if there was an interleaved mutation. In concurrent mode
// this can happen all the time, but even in synchronous mode, an earlier
// effect may have mutated the store.
if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
forceUpdate({inst});
}
}, [subscribe, value, getSnapshot]);
useEffect(() => {
// Check for changes right before subscribing. Subsequent changes will be
// detected in the subscription handler.
if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
forceUpdate({inst});
}
const handleStoreChange = () => {
// TODO: Because there is no cross-renderer API for batching updates, it's
// up to the consumer of this library to wrap their subscription event
// with unstable_batchedUpdates. Should we try to detect when this isn't
// the case and print a warning in development?
// The store changed. Check if the snapshot changed since the last time we
// read from the store.
if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
forceUpdate({inst});
}
};
// Subscribe to the store and return a clean-up function.
return subscribe(handleStoreChange);
}, [subscribe]);
return value;
}
function checkIfSnapshotChanged<T>(inst: {
value: T,
getSnapshot: () => T,
}): boolean {
const latestGetSnapshot = inst.getSnapshot;
const prevValue = inst.value;
try {
const nextValue = latestGetSnapshot();
return !is(prevValue, nextValue);
} catch (error) {
return true;
}
}
zustand源码分析
首先看仓库的创建
export interface StoreApi<T> {
setState: SetStateInternal<T>
getState: () => T
getInitialState: () => T
subscribe: (listener: (state: T, prevState: T) => void) => () => void
}
export type StateCreator<
T,
Mis extends [StoreMutatorIdentifier, unknown][] = [],
Mos extends [StoreMutatorIdentifier, unknown][] = [],
U = T,
> = ((
setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>,
getState: Get<Mutate<StoreApi<T>, Mis>, 'getState', never>,
store: Mutate<StoreApi<T>, Mis>,
) => U) & { $$storeMutators?: Mos }
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
const api = createStore(createState)
const useBoundStore: any = (selector?: any) => useStore(api, selector)
Object.assign(useBoundStore, api)
return useBoundStore
}
export const create = (<T>(createState: StateCreator<T, [], []> | undefined) =>
createState ? createImpl(createState) : createImpl) as Create
首先看create方法的入参,它的入参是StateCreator,看它的定义,它是一个函数,函数的入参有三个参数,setState对应
StoreApi中的setState,类型是SetStateInternal,getState对应StoreApi中的getState函数,store对应StoreApi实例对象。接着他会调用createImpl方法,首先调用createStore方法。
const createStoreImpl: CreateStoreImpl = (createState) => {
type TState = ReturnType<typeof createState>
type Listener = (state: TState, prevState: TState) => void
let state: TState
const listeners: Set<Listener> = new Set()
const setState: StoreApi<TState>['setState'] = (partial, replace) => {
// TODO: Remove type assertion once https://github.com/microsoft/TypeScript/issues/37663 is resolved
// https://github.com/microsoft/TypeScript/issues/37663#issuecomment-759728342
const nextState =
typeof partial === 'function'
? (partial as (state: TState) => TState)(state)
: partial
if (!Object.is(nextState, state)) {
const previousState = state
state =
(replace ?? (typeof nextState !== 'object' || nextState === null))
? (nextState as TState)
: Object.assign({}, state, nextState)
listeners.forEach((listener) => listener(state, previousState))
}
}
const getState: StoreApi<TState>['getState'] = () => state
const getInitialState: StoreApi<TState>['getInitialState'] = () =>
initialState
const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
listeners.add(listener)
// Unsubscribe
return () => listeners.delete(listener)
}
const api = { setState, getState, getInitialState, subscribe }
const initialState = (state = createState(setState, getState, api))
return api as any
}
这里其实是用来构建一个StoreApi,同时也为使用useSyncExternalStore做好准备,首先看下setState方法,里面会判断设置的state和原来的state会不会一致,不一致的话就会触发listener set里listener方法的调用,这个listener,就是subscribe函数里的入参函数,其实就是useSyncExternalStore传入的handleStoreChange函数,前面也分析过,当状态改变,调用handleStoreChange,就会触发重绘。
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
const api = createStore(createState)
const useBoundStore: any = (selector?: any) => useStore(api, selector)
Object.assign(useBoundStore, api)
return useBoundStore
}
createImpl接下来就是构建一个useBounStore,它是一个函数,在它的内部会调用useStore,而useStore,其实就是前面说的对useSyncExternalStore调用。
export function useStore<TState, StateSlice>(
api: ReadonlyStoreApi<TState>,
selector: (state: TState) => StateSlice = identity as any,
) {
const slice = React.useSyncExternalStore(
api.subscribe,
() => selector(api.getState()),
() => selector(api.getInitialState()),
)
React.useDebugValue(slice)
return slice
}
所以当我们create后会返回一个useBoundStore这个hook方法,再调用后就可以通过useSyncExternalStore拿到我们的对象。并且在调用StateCreator入参的setState方法时,触发更新。
最佳实践
- store结构
如果页面叫为简单单一,可以只建立一个store,使用全局store处理。如果业务较为复杂,可以从store使用范围上,store分为三类,app级别的store,page(页面)级别的store,以及component(组件)级别的store,同级之间的store不允许使用,上层的store允许使用下层的store,如果有同级别的状态需要通信,将状态下沉,组件到页面,页面到app。如上图。
- store的拆分
同级别的store,不宜过于庞大,可以拆分为不同的slic,然后合并处理
export interface StateA {
data: number;
asyncIncrease: (by: number) => Promise<void>;
increase: (by: number) => void;
removeAll: () => void;
}
export interface StateB {
num: number;
}
//设置状态b的slice
const createStateBSlice: StateCreator<StateA & StateB, [], [], StateB> = (set) => ({
num: 0
});
//设置状态A的slice
const createStateASlic: StateCreator<StateA & StateB, [], [], StateA> = (set) => ({
data: 1,
asyncIncrease: async (by: number) => {
await set((state) => ({ data: state.data + by }));
},
increase: (by) => set((state) => { return { data: state.data + by } }),
removeAll: () => set({ data: 0 })
});
//合并两个slice为一个store
const useStore = create<StateA & StateB>()((...a)=>({
...createStateASlic(...a),
...createStateBSlice(...a)
}))