홍승아블로그

zustand

참고코드: GitHub - seungahhong/states-todos

설치

npm install zustand

or

yarn add zustand

zustand 개념

  • 상태관리 설명 요즘 상태관리에 대한 접근 방식을 3가지로 나눠서 볼 수 있을 것 같습니다.
    • Flux (Redux, Zustand)
      • 일반적으로 사용하는 Flux 방식으로써, 저장소(store)/액션함수(action)/리듀서 등을 통해서 상태를 업데이트 하는 방식
    • Proxy (Mobx, Valtio)
      • 컴포넌트에 사용되는 일부 상태를 자동으로 감지해서 업데이트 하는 방식
    • Atomic (Jotai, Recoil)
      • React에 사용되는 state와 비슷하게 리액트 트리 안에서 상태를 저장하고 관리하는 방식
  • zustand 개념
    • store 구현 방식 및 변경 방식이 간단함(redux에 비해 보일러플레이트 코드가 적다)
    • Provider로 래핑할 필요가 없다.
    • Context API를 사용할 때와 달리 상태 변경 시 불필요한 리렌더링일 일어나지 않는다.(변경된 상태만 업데이트)
    • 특정 라이브러리에 엮이지 않는다(React와 함께 쓸 수 있는 API는 기본적으로 제공 → hooks 제공)
    • 한 개의 중앙에 집중된 형식의 스토어 구조를 활용하면서, 상태를 정의하고 사용하는 방법이 단순하다.
    • redux보다는 flux 구조와 유사함.
    • 다양한 middleware 라이브러리를 제공(redux, immer, selector(상태변경 시 자동 호출), redux-devtools)

zustand 특징

  • Fetching everything store안에 상태값들 모두 얻어올 순 있지만, 모든 상태가 변경될 떄마다 컴포넌트가 업데이트 됨에따라서 선택해서 상태값을 얻어오도록 해야함.

    const { todo, fetchTodo, fetchTodos } = useStore();
  • Selecting multiple state slices 기본적으로 strict compare(old === new) 변경될 시 컴포넌트를 렌더링 합니다. redux의 useSelector, jotai의 selectAtom, recoil의 selector 기능과 동일

    const todo = useStore(state => state.todo);
    const fetchTodo = useStore(state => state.fetchTodo);
    const fetchTodos = useStore(state => state.fetchTodos);
  • Memoizing selectors React 함수형 컴포넌트에서 useCallback과 같이 사용한다면 selector를 메모할 수 있습니다. 이렇게 하면 렌더링할 때마다 불필요한 계산이 방지됩니다.

    const todo = useStore(useCallback((state) => state.todo, [todo])
  • Using subscribe with selector 상태관리를 구독해서 상태값이 변경될 경우 호출될 수 있도록 처리가 가능합니다. mobx의 autorun/computed의 동일한 기능과 동일

    import { subscribeWithSelector } from "zustand/middleware";
    export const useStore = create<TodoState>()(
    	subscribeWithSelector(
        todo: {
          todoItems: [],
          loading: true,
          error: false,
        },
    	)
    	);
    
    // todoItems가 변경될 경우 subscribe 2번째 인자의 callback 함수 호출
    useEffect(() => {
      // const todoLength = useMemo(() => todoItems.length, [todoItems]);
      useStore.subscribe(
        (state) => state.todo.todoItems,
        (todoItems) => console.log(todoItems.length)
      );
    
      return () => {
        useStore.destroy();
      };
    }, []);
    
  • Async actions

    fetchTodos: async () => {
      set((state) => {
        state.todo.loading = true;
      });
    
      try {
        const response = await fetchTodo();
        set((state) => {
          state.todo.loading = false;
          state.todo.todoItems = [].concat(response.data);
        });
      } catch (e) {
        set((state) => {
          state.todo.loading = false;
          state.todo.error = e;
        });
      }
    },
  • Read from state in actions

    const useStore = create((set, get) => ({
      todo: {
        todoItems: [],
        loading: true,
        error: false,
      },
      action: () => {
        const todo = get().todo
        // ...
      }
    })
  • Immer middleware vs npm Immer Immer 라이브러리를 사용가능지만, Zustand middleware Immer 사용가능함.

    import { immer } from 'zustand/middleware/immer';
    export const useStore = create<TodoState>()(
      immer(set => ({
        todo: {
          todoItems: [],
          loading: true,
          error: false,
        },
        fetchTodos: async () => {
          set(state => {
            state.todo.loading = true;
          });
    
          try {
            const response = await fetchTodo();
            set(state => {
              state.todo.loading = false;
              state.todo.todoItems = [].concat(response.data);
            });
          } catch (e) {
            set(state => {
              state.todo.loading = false;
              state.todo.error = e;
            });
          }
        },
        fetchTodo: async (id: number) => {
          set(state => {
            return produce(state, draft => {
              draft.todo.loading = true;
            });
          });
    
          try {
            const response = await fetchTodo(id);
            set(state => {
              return produce(state, draft => {
                draft.todo.loading = false;
                draft.todo.todoItems = [].concat(response.data);
              });
            });
          } catch (e) {
            set(state =>
              produce(state, draft => {
                draft.todo.loading = false;
                draft.todo.error = e;
              }),
            );
          }
        },
      })),
    );
  • Redux devtools

    import { devtools } from 'zustand/middleware';
    
    const useStore = create(devtools(store));

zustand 그외 특징

  • Using zustand without React :

GitHub - pmndrs/zustand: 🐻 Bear necessities for state management in React

  • Transient updates (for often occurring state-changes)

GitHub - pmndrs/zustand: 🐻 Bear necessities for state management in React

  • Middleware

GitHub - pmndrs/zustand: 🐻 Bear necessities for state management in React

  • Overwriting state

GitHub - pmndrs/zustand: 🐻 Bear necessities for state management in React

  • React context:

GitHub - pmndrs/zustand: 🐻 Bear necessities for state management in React

개인의견

  • 상태관리(redux, contextapi)에 단점을 해소하기 위한다면 충분히 검토해볼만한 라이브러리 같습니다.
    • 보일러플레이트 코드 최소화(action, reducer, state, type 등등)
    • provider 래핑할 필요없음
    • 자식 리렌더링을 상태별로 업데이트 가능
  • redux,mobx 상태관리 보다는 쉽게 학습이 가능하고, 빠르게 도입해볼 수 있을 것 같습니다.
  • zustand 자체에서 지원하는 라이브러리가 많아서 쉽게 사용이 가능할 것 같습니다.(단, 라이브러리 풀지원이 되는지 여부 확인)
  • 리액트의 hooks 기반의 구조로 되어 있어서 확장성, 재사용성에 좋은 것 같습니다.

참고페이지

https://github.com/pmndrs/zustand

리액트 상태관리에 관한 얕은 고찰

React 상태 관리 라이브러리 Zustand의 코드를 파헤쳐보자

[React🌀] 차세대 상태관리 라이브러리, Jotai VS Zustand ⭐ (Feat. Recoil)

Zustand 번역

이전글
react-query(v5)
다음글
react-query(v4)