zustand는 기본적으로 새로 고침 시 데이터들이 날아감.
zustand persist
전역 상태를 사용하는데, 새로 고침 후에도 값을 사용하도록 스토리지에 값을 저장해야하는 경우 persist를 이용하면 된다.
"Persist 미들웨어를 사용하면 Zustand 상태를 저장소(예: localStorage, AsyncStorage, IndexedDB등)에 저장하여 해당 데이터를 유지할 수 있다."
zustand를 immer와 persist라는 내장 미들웨어가 있습니다.
create 함수 내에 persist(()=> (), { }) 형태로 작성
immer를 이용해 복잡한 객체의 업데이트를 간단히 처리할 수 있습니다.
create 함수 안에 immer로 감싸기만 하면 됩니다.
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer' // npm install immer 필요
const useBeeStore = create(
immer((set) => ({ // <- 바로 요기 부분
bees: 0,
addBees: (by) =>
set((state) => {
state.bees += by
}),
}))
)
다음과 같이 persist를 이용해 storage에 저장할 수 있고 로컬 스토리지뿐만 아니라 세션스토리지도 지원합니다.
(Recoil에서는 새로고침시에 데이터 유지를 위해 recoil-persist라는 파생라이브러리르 사용하거나 별도의 로직을 만들어 저장하는 과정을 수행해야 했습니다.)
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', // unique name
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
}
)
)
고수 팁 ) 비구조화 할당 문법으로 원하는 것만 가져다 쓰기 가능.
// e.g.
import { shallow } from 'zustand/shallow'
// 일반 사용법
const { increment } = useCounter();
// 고수 사용법
const increment = useCounter(state => state.increment);
// 고수 사용법 2 shallow
const { increment, decrement } = useCounter((state) => ({
increment: state.increment,
decrement: state.decrement,
}),shallow);
이 두 개의 차이점은 비교로직 차이입니다.
react의 비교로직과 달리 state로 가져온 고수 사용법은 === 연산자를 쓰기 때문에 조금 더 효율적인 랜더링이 가능합니다. ( zustand가 내세우는 장점 중에 하나입니다 )
타입스크립트 이용해 간단하게 초기화 하기.
// CounterStore.tsx
import create from "zustand";
interface CounterState {
count: number;
}
const intialState = {
count: 0,
};
export interface CounterStore extends CounterState {
increment: () => void;
decrement: () => void;
resetCounterStore: () => void;
}
const useCounterStore = create<CounterStore>((set) => ({
...intialValue,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
resetCounterStore: () => set(intialState),
}));
export default useCounterStore;
initalState를 파일 내부에 선언하고 reset에서 값을 업데이트해 주는 형식입니다.
그래서 초기화함수를 활용할 수도 있고 코드를 분리함으로써 초기데이터를 읽기도 쉬워졌습니다.
타입스크립트와 함께 쓰기.
// state와ㅗ action을 동등한 계층으로 사용해본 예시.
// CounterStore.tsx
// state와 actions로 분리한 type 사용예시
type State = {
count: number;
};
type Actions = {
increment: () => void;
decrement: () => void;
resetCounterStore: () => void;
};
const initialState: State = {
count: 0,
};
const useCounterStore = create<State & Actions>((set) => ({
...initialState,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
resetCounterStore: () => set(initialState),
}));
export default useCounterStore;
파생 데이터 다루기
const useStore = create((set) => ({
first: 'Daishi',
last: 'Kato',
get fullName() {
return `${this.first} ${this.last}`;
},
}));
//이렇게 하면 computed 값 같은 경우는 골치가 아픕니다.
// 공식 지원은 아니고 별도의 라이브러리 (zustand-middleware-computed-state)가 있지만 기본 라이브러리만 사용해서 작업한다며 ㄴ아래와 같이 차라리 별도로 빼서 하는 방법도 있다.
import create from 'zustand';
import { useMemo } from "react";
// 선언
const useCounter = create((set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 }))
}));
const useCounterDerived = () => {
const state = useCounter();
const compuntedCount = useMemo(() => {
// ... 생략
}, [state.count]);
return {
compuntedCount
}
};