zustand
Reference code: GitHub - seungahhong/states-todos
Installation
npm install zustand
or
yarn add zustandzustand Concepts
- State management overview
These days, approaches to state management can roughly be divided into three categories.
- Flux (Redux, Zustand)
- The commonly used Flux approach, which updates state through a store, action functions, reducers, and so on.
- Proxy (Mobx, Valtio)
- An approach that automatically detects and updates the specific pieces of state used by a component.
- Atomic (Jotai, Recoil)
- An approach that stores and manages state within the React tree, similar to React's own state.
- Flux (Redux, Zustand)
- zustand concepts
- The way stores are implemented and updated is simple (less boilerplate code compared to Redux).
- There is no need to wrap the app with a Provider.
- Unlike when using the Context API, unnecessary re-renders do not occur when state changes (only the changed state is updated).
- It is not tightly coupled to any particular library (it provides APIs that work with React out of the box → hooks are provided).
- While leveraging a single centralized store structure, defining and using state is simple.
- It is more similar to the Flux structure than to Redux.
- It provides a variety of middleware libraries (redux, immer, selector (automatically called when state changes), redux-devtools).
zustand Features
-
Fetching everything You can retrieve all state values from the store, but since the component updates every time any state changes, you should select and retrieve only the state values you need.
const { todo, fetchTodo, fetchTodos } = useStore(); -
Selecting multiple state slices By default, the component re-renders when a strict compare (old === new) detects a change. This is the same as redux's useSelector, jotai's selectAtom, and recoil's selector.
const todo = useStore(state => state.todo); const fetchTodo = useStore(state => state.fetchTodo); const fetchTodos = useStore(state => state.fetchTodos); -
Memoizing selectors If you use it together with useCallback in a React functional component, you can memoize the selector. This prevents unnecessary computations on every render.
const todo = useStore(useCallback((state) => state.todo, [todo]) -
Using subscribe with selector You can subscribe to the state management so that a callback is invoked when a state value changes. This is the same as mobx's autorun/computed functionality.
import { subscribeWithSelector } from "zustand/middleware"; export const useStore = create<TodoState>()( subscribeWithSelector( todo: { todoItems: [], loading: true, error: false, }, ) ); // When todoItems changes, the callback passed as the second argument to subscribe is invoked 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 You can use the Immer library, but you can also use Zustand's Immer middleware.
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));
Other zustand Features
- 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
Personal Opinion
- If you want to resolve the drawbacks of state management (redux, contextapi), this library seems well worth considering.
- Minimizes boilerplate code (action, reducer, state, type, etc.)
- No need to wrap with a provider
- Child re-renders can be updated per state
- It is easier to learn than redux or mobx state management, and seems quick to adopt.
- Since zustand itself supports many libraries, it seems easy to use. (However, check whether full library support is available.)
- Because it is built on a React hooks-based structure, it seems good for extensibility and reusability.
References
https://github.com/pmndrs/zustand
React 상태 관리 라이브러리 Zustand의 코드를 파헤쳐보자