Zustand state management
Апп томрох тусам олон component дундаа state хуваалцах хэрэгтэй болдог. useState нь нэг component-д, useContext нь дунд хэмжээний хуваалцсан state-д тохиромжтой. Гэхдээ маш их газарт өөрчлөгдөх глобал state-д Zustand хамгийн зохистой сонголт. Ойлгоход хялбар, кодны хэмжээ бага, хурдан!
Яагаад Zustand вэ?
Redux нь хүчирхэг боловч их boilerplate code шаарддаг. Context нь хялбар боловч гүйцэтгэлийн асуудал гардаг — value өөрчлөгдөх болгонд бүх subscriber дахин рендерлэгдэнэ. Zustand хоёрын давуу талыг хослуулсан:
npm install zustand
Анхны store үүсгэх
import { create } from "zustand";
// Store нь state + action-уудыг агуулна
const useCounterStore = create((set) => ({
count: 0,
// set нь state-г шинэчлэх функц
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Ямар ч component-д provider хэрэггүйгээр шууд хэрэглэнэ
function Counter() {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
const reset = useCounterStore((state) => state.reset);
return (
<div style={{ textAlign: "center", padding: "24px" }}>
<p style={{ fontSize: "48px", fontWeight: "bold" }}>{count}</p>
<div style={{ display: "flex", gap: "8px", justifyContent: "center" }}>
<button onClick={decrement}>−</button>
<button onClick={reset}>Дахин тохируулах</button>
<button onClick={increment}>+</button>
</div>
</div>
);
}
Provider хэрэгтэй гэж байхгүй — store-г аппын хаанаас ч шууд ашиглаж болно. useCounterStore((state) => state.count) нь зөвхөн count өөрчлөгдөхөд л дахин рендерлэдэг — гүйцэтгэл сайн байдгийн нууц энэ!
Бодит жишээ: сагс (cart store)
import { create } from "zustand";
const useCartStore = create((set, get) => ({
items: [],
// get() нь одоогийн state-г уншина
addItem: (product) => {
const items = get().items;
const existing = items.find((i) => i.id === product.id);
if (existing) {
set({
items: items.map((i) =>
i.id === product.id ? { ...i, qty: i.qty + 1 } : i,
),
});
} else {
set({ items: [...items, { ...product, qty: 1 }] });
}
},
removeItem: (id) => set({ items: get().items.filter((i) => i.id !== id) }),
updateQty: (id, qty) =>
set({
items:
qty <= 0
? get().items.filter((i) => i.id !== id)
: get().items.map((i) => (i.id === id ? { ...i, qty } : i)),
}),
clearCart: () => set({ items: [] }),
// Computed утга — selector ашиглана
get total() {
return get().items.reduce((s, i) => s + i.price * i.qty, 0);
},
get itemCount() {
return get().items.reduce((s, i) => s + i.qty, 0);
},
}));
// Сагсны товчлуур — зөвхөн itemCount-г subscribe хийнэ
function CartIcon() {
const itemCount = useCartStore((state) =>
state.items.reduce((s, i) => s + i.qty, 0),
);
return (
<div style={{ position: "relative", display: "inline-block" }}>
🛒
{itemCount > 0 && (
<span
style={{
position: "absolute",
top: "-8px",
right: "-8px",
background: "red",
color: "white",
borderRadius: "50%",
width: "20px",
height: "20px",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "12px",
}}
>
{itemCount}
</span>
)}
</div>
);
}
// Бүтээгдэхүүний карт
function ProductCard({ product }) {
const addItem = useCartStore((state) => state.addItem);
return (
<div
style={{ border: "1px solid #ccc", padding: "16px", borderRadius: "8px" }}
>
<h3>{product.name}</h3>
<p>{product.price.toLocaleString()}₮</p>
<button onClick={() => addItem(product)}>Сагсанд нэмэх</button>
</div>
);
}
Selector — зөвхөн хэрэгтэй хэсгийг subscribe хийх
Zustand-н гол давуу тал бол нарийн selector:
const useAuthStore = create((set) => ({
user: null,
token: null,
isLoading: false,
login: async (email, password) => {
set({ isLoading: true });
try {
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const { user, token } = await res.json();
set({ user, token, isLoading: false });
} catch {
set({ isLoading: false });
}
},
logout: () => set({ user: null, token: null }),
}));
// Navbar — зөвхөн user.name-г сонирхоно
// user.name өөрчлөгдөхөд л дахин рендерлэгдэнэ
function Navbar() {
const userName = useAuthStore((state) => state.user?.name);
const logout = useAuthStore((state) => state.logout);
return (
<nav>
{userName ? (
<>
<span>Сайн уу, {userName}!</span>
<button onClick={logout}>Гарах</button>
</>
) : (
<a href="/login">Нэвтрэх</a>
)}
</nav>
);
}
// LoginButton — зөвхөн isLoading-г сонирхоно
function LoginButton() {
const isLoading = useAuthStore((state) => state.isLoading);
const login = useAuthStore((state) => state.login);
return (
<button disabled={isLoading} onClick={() => login("test@test.com", "pass")}>
{isLoading ? "Нэвтэрч байна..." : "Нэвтрэх"}
</button>
);
}
Navbar нь зөвхөн user?.name өөрчлөгдөхөд л дахин рендерлэгдэнэ — isLoading өөрчлөгдсөн ч Navbar хөдлөхгүй. Context ашигласан бол хоёулаа дахин рендерлэгдэх байсан.
Дараагийн хичээлд:
React + TypeScript үндэс сурна — React component-уудыг TypeScript-ээр хэрхэн бичихийг ойлгоно. Props, state, event зэрэгт төрөл (type) тодорхойлох нь алдааг эрт илрүүлж, кодыг найдвартай болгодог.