Skip to main content

Zustand Basic

Example

Basic1 (create, set)

import { create } from 'zustand'

const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
---
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}

function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}

// define store, setter
import { create } from 'zustand';

interface AuthModalStore {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}

const useAuthModal = create<AuthModalStore>((set) => ({
isOpen: false,
onOpen: () => set({ isOpen: true }),
onClose: () => set({ isOpen: false }),
}));

export default useAuthModal;
---
const authModal = useAuthModal();
<Button onClick={authModal.onOpen} />

State & Action 분리 타이핑

import { ReactNode } from "react";
import { create } from "zustand";

type ModalConfig = {
title: string;
description?: string;
content?: ReactNode;
footer: ReactNode;
};

type State = {
open: boolean;
config?: ModalConfig;
};

type Action = {
openModal: (config: ModalConfig) => void;
closeModal: () => void;
};

const useModalStore = create<State & Action>((set) => ({
open: false,
config: undefined,
openModal: (config) => set({ open: true, config }),
closeModal: () => set({ open: false, config: undefined }),
}));

export { useModalStore };

Counter

import { create } from "zustand";
import { debounce } from "lodash-es";

interface CounterStoreProps {
count: number;
increaseCount: () => void;
decreaseCount: () => void;
}

export const counterStore = create<CounterStoreProps>((set) => ({
count: 0,
increaseCount: () => set((state) => ({ count: state.count + 1 })),
decreaseCount: () => set((state) => ({ count: state.count - 1 })),
}));

TodoList

  • 객체 안의 객체(todoListArray)의 불변성을 지켜야한다. (for 리랜더링)
// todoList
interface TodoListProps {
todoList: string[];
addTodo: (content: string) => void;
updateTodo: (id: number, updatedContent: string) => void;
deleteTodo: (id: number) => void;
}

export const todoListStore = create<TodoListProps>((set) => ({
todoList: [],
addTodo: (content: string) =>
set(({ todoList }) => {
return { todoList: [...todoList, content] };
}),
updateTodo: (id: number, updatedContent: string) =>
set(({ todoList }) => {
todoList[id] = updatedContent;
return { todoList: [...todoList] };
}),
deleteTodo: (id: number) =>
set(({ todoList }) => {
todoList.splice(id, 1);
return { todoList: [...todoList] };
}),
}));

Fetch (create, AbortController, AxiosError, debouncedFetch)

기존의 요청을 취소하고 새로운 요청을 보내는 로직.

  • abort를 호출했다면 AbortController를 다시 생성해야한다.
  • 그렇지 않으면, 기존요청 및 새로운 요청에 대해서 모두 취소 된다.
import { create } from "zustand";
import { debounce } from "lodash-es";
import axios, { AxiosError } from "axios";

// https://api.thecatapi.com/v1/images/search?limit=1
interface RandomCatState {
id: string;
url: string;
width: number;
height: number;
loading: boolean;
error: null | AxiosError;
fetchData: (id?: string) => void;
debouncedFetch: () => void;
}

export const useRandomCatStore = create<RandomCatState>((set, get) => {
let controller = new AbortController();

return {
id: "0",
url: "",
width: 0,
height: 0,
loading: false,
error: null,
fetchData: async (id?: string) => {
if (get().loading) controller.abort();

controller = new AbortController();
set({ loading: true });
try {
const result = await axios.get(
"https://api.thecatapi.com/v1/images/search?limit=1",
{
signal: controller.signal,
}
);
set({ ...result.data?.[0], loading: false });
} catch (error) {
assertAxiosError(error);
if (error?.code === "ERR_CANCELED") {
return;
}
set({ loading: false, error });
}
},
debouncedFetch: debounce(() => get().fetchData(), 200), // debouncing
};
});

vanlia debounce, debounced state

export function debounce<T>(func: (args: T) => void, delay: number) {
let timer: number;

return (args: T) => {
if (timer) clearTimeout(timer);

timer = window.setTimeout(() => {
func(args);
}, delay);
};
}
---
import { debounce } from "@/utils/debounce";
import { create } from "zustand";

interface SearchAreaStore {
keywordInput: string;
setKeywordInput: (keywordInput: string) => void;
debouncedKeyword: string;
setDebouncedKeyword: (keywordInput: string) => void;
reset: () => void;
}

const defaultState: Pick<SearchAreaStore, "keywordInput" | "debouncedKeyword"> =
{
keywordInput: "",
debouncedKeyword: "",
};

export const useStoreSearchArea = create<SearchAreaStore>((set, get) => ({
keywordInput: "",
setKeywordInput: (keywordInput: string) => {
set({ keywordInput });
get().setDebouncedKeyword(keywordInput);
},
debouncedKeyword: "",
setDebouncedKeyword: debounce((keywordInput: string) => {
set({ debouncedKeyword: keywordInput });
}, 500),
reset: () => {
set({ ...defaultState });
},
}));

ref