Zustand 리덕스에 영감을 받아 만들어졌다. 하나의 스토어를 중앙 집중형으로 활용해 이 스토어 내부에서 상태를 관리하고 있다.
(2022년 9월 기준 Zustand 최신 버전인 4.1.1을 기준으로 한다.)
// Zustand store 코드
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: SetStateInternal<TState> = (partial, replace) => {
// ...
const nextState =
typeof partial === 'function' ? (partial as(state:TState) => Tstate(state) : partial
if (nextState !== state) {
const previousState =state
state =
replace ?? typeof nextState !== 'object' ? (nextState as TState) : Object.assign({}, state, nextState)
listeners.forEac((listener) => listener(state, previousState))
}
}
const getState: () => TState = () => state
const subscribe: (listener: Listener) => () => void = (listener) => {
listeners.add(listener)
//Unsubscribe
return () => listeners.delete(listener)
}
const destroy: () => void = () => listeners.clear()
cosnt api = { setState, getState, subscribe, destroy }
state = ( createState as PopArgument<typeof createState>)(setState, getState, api,
)
return api as any
}
//./src/vanilla.ts
type CounterStore = {
count: number
increase: (num: number) => void
}
const store = createStore<CounterStore>((set) => ({
count: 0,
increase: (num: number) => set((state) => ({ count: state.count + num })), }))
store.subscribe((state, prev) => {
if (state.count !== prev.count) {
console.log('count has been changed', state.count)
}
})
store.setState((state) => ({ count : state.count +1 }))
store.getState().increase()
Zustand를 리액트에서 사용하기 위해서는 어디선가 store를 읽고 리렌더링을 해야 한다. Zustand 스토어를 리액트에서 사용할 수 있도록 도와주는 함수들은 ./src/react/ts에서 관리되고 있다. 타입을 제외하고 여기에서 export하는 함수는 바로 useStore와 create다. 먼저 useStore를 살펴보자.
// Zustand의 useStore 구현
export function useStore<TState, StateSlice>(
api: WithReact<StoreApi<TState>>,
selector : (state: TState) => StateSlice = api.getState as any,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean,
) {
const slice = useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getSrverState || api.getState,
selector,
equalityFn,
)
useDebugValue(slice)
return slice
}
또 한 가지, ./src/react.ts에서 export하는 변수는 바로 create인데, 이는 리액트에서 사용할 수 있는 스토어를 만들어주는 변수다.
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
const api =
typeof createState === 'function' ? createStore(createState) : createState
const useBoundStore: any = (selector?: any, equalityFn?: any) => useStore(api, selector, equalityFn)
Object.assign(useBoundStore, api)
return useBoundStore
}
const create = (<T>(createState: StateCreator<T, [], []> | undefined) => createState ? createImpl(createState) : createImpl) as Create
export default create
리액트의 create는 바닐라 createStore를 기반으로 만들어졌기 때문에 거의 유사하다. 또한 간결한 구조 덕분에 리액트 환경에서도 스토어를 생성하고 사용하기가 매우 쉽다.
interface Store {
count: number
text: string
increase: (count: number) => void
setText: (text: string) => void
}
const store = createStore<Store>((set) => ({
count: 0,
text: '',
increase: (num) => set((state) => ({ count: state.count + num })),
setText: (text) => set({ text}),
}))
const counterSelector = ({ count, increase }: Store) => ({
count, increase,
})
function Counter() {
const{ count, increase} = useStore(store, counterSelector)
function handleClick() {
increase(1)
}
return (
<>
<h3>{count}</h3>
<button onClick={handleClick}>+</button>
</>
)
}
const inputSelector = ({ text, setText}: Store) => ({
text,
setText,
})
function Input() {
const {text, setText} = useStore(store, inputSelector)
useEffect(() => {
console.log('Input Changed')
})
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setText(e.target.value)
}
return (
<div>
<input value={text} onChange={handleChange} />
</div>
)
}
스토어 생성 자체는 앞선 예제와 동일하며, useStore를 사용하면 이 스토어를 리액트에서 사용할 수 있게 된다.
create를 사용해 스토어를 만들면 useStore를 굳이 사용하지 않더라도 바로 사용 가능하다.
import {create} from 'zustand'
const useCounterStore = create((set) => ({
count: 1,
inc: () => set((state) => ({count:state,count +1})),
dec: () => set((state) => ({count: state.count -1})),
}))
function Counter() {
const { count, inc, dec } = useCounterStore()
return (
<div class="counter">
<span>{count}</span>
<button onClick={inc}>up</button>
<button onClick={dec}>down</button>
</div>
)
}
Zustand의 create를 통해 스토어를 만들고 반환 값으로 이 스토어를 컴포넌트 내부에서 사용할 수 있는 훅을 받았다. 이 훅으로 getter, setter 모두 접근해 사용 가능하게 된다.
Zustand는 특별히 많은 코드를 작성하지 않아도 빠르게 스토어를 만들고 사용할 수 있다는 큰 장점이 있다. 라이브러리 크기 역시 Bundlephobia 기준 79.1kB인 Recoil, 13.1kB인 Jotai와 다르게 Zustand는 고작 2.9kB이다.
또한 미들웨어 역시 지원하는데, create의 두 번째 인수로 원하는 미들웨어를 추가하면 된다. 스토어 데이터 영구 보존할 수 있는 persist, 복잡한 객체 관리 도와주는 immer, 리덕스 미들웨어 등의 사용이 가능하다.