7.7 ניהול סטייט Zustand הרצאה
ניהול סטייט - Zustand¶
בשיעור הזה נלמד על Zustand - ספריית ניהול state קלה ופשוטה לריאקט. Zustand מציעה API מינימלי עם יכולות חזקות, והיא אחת מספריות ניהול ה-state הפופולריות ביותר כיום.
למה ניהול סטייט חיצוני?¶
המגבלות של Context + useReducer¶
- כל שינוי בקונטקסט מרנדר מחדש את כל הצרכנים
- אין selectors - לא ניתן להאזין רק לחלק מה-state
- ביצועים ירודים באפליקציות גדולות עם עדכונים תכופים
- אין middleware מובנה (persist, devtools, logging)
למה Zustand?¶
- API פשוט מאוד - פחות boilerplate מ-Redux
- ביצועים טובים - רנדר רק כשהנתון שמאזינים לו משתנה
- גודל קטן (כ-1KB)
- עובד גם מחוץ לריאקט
- Middleware מובנה (persist, devtools, immer)
- אין צורך ב-Provider
התקנה¶
יצירת Store בסיסי¶
import { create } from "zustand";
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
incrementBy: (amount: number) => void;
}
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
incrementBy: (amount) => set((state) => ({ count: state.count + amount })),
}));
createמקבל פונקציה שמגדירה את ה-state והפעולותsetמעדכן את ה-state (כמו setState, אבל מבצע merge רדוד)- ה-store מוחזר כהוק
שימוש בקומפוננטות¶
function Counter() {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
return (
<div>
<p>מונה: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
function ResetButton() {
const reset = useCounterStore((state) => state.reset);
return <button onClick={reset}>אפס</button>;
}
- כל קומפוננטה בוחרת את הנתונים שהיא צריכה באמצעות selector
- הקומפוננטה תרנדר מחדש רק כשהנתון שנבחר משתנה
- אין צורך ב-Provider!
Store מתקדם יותר¶
חנות מוצרים¶
import { create } from "zustand";
interface Product {
id: number;
name: string;
price: number;
}
interface CartItem extends Product {
quantity: number;
}
interface StoreState {
cart: CartItem[];
isCartOpen: boolean;
// פעולות
addToCart: (product: Product) => void;
removeFromCart: (productId: number) => void;
updateQuantity: (productId: number, quantity: number) => void;
clearCart: () => void;
toggleCart: () => void;
// ערכים נגזרים (computed)
getTotalItems: () => number;
getTotalPrice: () => number;
}
const useStore = create<StoreState>((set, get) => ({
cart: [],
isCartOpen: false,
addToCart: (product) =>
set((state) => {
const existing = state.cart.find((item) => item.id === product.id);
if (existing) {
return {
cart: state.cart.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
}
return { cart: [...state.cart, { ...product, quantity: 1 }] };
}),
removeFromCart: (productId) =>
set((state) => ({
cart: state.cart.filter((item) => item.id !== productId),
})),
updateQuantity: (productId, quantity) =>
set((state) => ({
cart:
quantity <= 0
? state.cart.filter((item) => item.id !== productId)
: state.cart.map((item) =>
item.id === productId ? { ...item, quantity } : item
),
})),
clearCart: () => set({ cart: [] }),
toggleCart: () => set((state) => ({ isCartOpen: !state.isCartOpen })),
// שימוש ב-get לגישה ל-state הנוכחי
getTotalItems: () => {
return get().cart.reduce((sum, item) => sum + item.quantity, 0);
},
getTotalPrice: () => {
return get().cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
},
}));
getמאפשר גישה ל-state הנוכחי (שימושי ב-computed values)setמבצע merge רדוד - צריך להחזיר רק את השדות שמשתנים
שימוש¶
function CartBadge() {
const totalItems = useStore((state) => state.getTotalItems());
return <span>סל ({totalItems})</span>;
}
function CartDrawer() {
const cart = useStore((state) => state.cart);
const isOpen = useStore((state) => state.isCartOpen);
const removeFromCart = useStore((state) => state.removeFromCart);
const updateQuantity = useStore((state) => state.updateQuantity);
const getTotalPrice = useStore((state) => state.getTotalPrice);
if (!isOpen) return null;
return (
<div>
<h2>סל קניות</h2>
{cart.map((item) => (
<div key={item.id}>
<span>{item.name}</span>
<span>{item.price} ש"ח</span>
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
<span>{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
<button onClick={() => removeFromCart(item.id)}>הסר</button>
</div>
))}
<p>סה"כ: {getTotalPrice()} ש"ח</p>
</div>
);
}
Middleware - תוספים¶
Persist - שמירה אוטומטית¶
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
interface SettingsStore {
theme: "light" | "dark";
language: string;
fontSize: number;
setTheme: (theme: "light" | "dark") => void;
setLanguage: (language: string) => void;
setFontSize: (size: number) => void;
}
const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
theme: "light",
language: "he",
fontSize: 16,
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
setFontSize: (fontSize) => set({ fontSize }),
}),
{
name: "settings-storage", // מפתח ב-localStorage
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
// שומר רק חלק מה-state
theme: state.theme,
language: state.language,
fontSize: state.fontSize,
}),
}
)
);
- persist שומר את ה-state ב-localStorage (או sessionStorage) אוטומטית
- partialize מאפשר לבחור אילו שדות לשמור
- ה-state משוחזר אוטומטית בטעינה הבאה
DevTools - כלי פיתוח¶
import { create } from "zustand";
import { devtools } from "zustand/middleware";
const useStore = create<StoreState>()(
devtools(
(set) => ({
count: 0,
increment: () =>
set((state) => ({ count: state.count + 1 }), false, "increment"),
// הפרמטר השלישי של set הוא שם הפעולה ב-devtools
}),
{ name: "MyStore" }
)
);
- עובד עם Redux DevTools Extension בדפדפן
- מאפשר לעקוב אחרי כל שינוי ב-state, time-travel debugging ועוד
- הפרמטר השלישי ב-set מגדיר את שם הפעולה שיוצג ב-devtools
Immer - עדכון state אימיוטבלי בקלות¶
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
interface TodoStore {
todos: { id: number; text: string; done: boolean }[];
addTodo: (text: string) => void;
toggleTodo: (id: number) => void;
removeTodo: (id: number) => void;
}
const useTodoStore = create<TodoStore>()(
immer((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
// אפשר לשנות את state ישירות - immer מטפל באימיוטביליות
state.todos.push({
id: Date.now(),
text,
done: false,
});
}),
toggleTodo: (id) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
}),
removeTodo: (id) =>
set((state) => {
const index = state.todos.findIndex((t) => t.id === id);
if (index !== -1) state.todos.splice(index, 1);
}),
}))
);
- בלי immer:
set(state => ({ todos: state.todos.map(t => t.id === id ? {...t, done: !t.done} : t) })) - עם immer:
set(state => { const todo = state.todos.find(t => t.id === id); if(todo) todo.done = !todo.done; }) - הרבה יותר קריא, במיוחד עם state מקונן
שילוב Middleware¶
const useStore = create<StoreState>()(
devtools(
persist(
immer((set) => ({
// ...store definition
})),
{ name: "my-storage" }
),
{ name: "MyStore" }
)
);
Slices - פיצול ה-Store¶
באפליקציות גדולות, מומלץ לפצל את ה-store ל-slices:
import { create, StateCreator } from "zustand";
// Slice - משתמשים
interface UserSlice {
user: { name: string; email: string } | null;
login: (name: string, email: string) => void;
logout: () => void;
}
const createUserSlice: StateCreator<
UserSlice & CartSlice,
[],
[],
UserSlice
> = (set) => ({
user: null,
login: (name, email) => set({ user: { name, email } }),
logout: () => set({ user: null, cart: [] }), // ניגש גם ל-cart slice
});
// Slice - עגלת קניות
interface CartSlice {
cart: { id: number; name: string; quantity: number }[];
addToCart: (item: { id: number; name: string }) => void;
clearCart: () => void;
}
const createCartSlice: StateCreator<
UserSlice & CartSlice,
[],
[],
CartSlice
> = (set) => ({
cart: [],
addToCart: (item) =>
set((state) => ({
cart: [...state.cart, { ...item, quantity: 1 }],
})),
clearCart: () => set({ cart: [] }),
});
// חיבור כל ה-Slices
const useStore = create<UserSlice & CartSlice>()((...args) => ({
...createUserSlice(...args),
...createCartSlice(...args),
}));
// שימוש
function Header() {
const user = useStore((state) => state.user);
const logout = useStore((state) => state.logout);
const cartCount = useStore((state) => state.cart.length);
return (
<header>
{user ? (
<>
<span>שלום, {user.name}</span>
<span>סל ({cartCount})</span>
<button onClick={logout}>התנתק</button>
</>
) : (
<span>אורח</span>
)}
</header>
);
}
Zustand מול Context¶
| מאפיין | Context + useReducer | Zustand |
|---|---|---|
| גודל | מובנה בריאקט | כ-1KB |
| Provider | נדרש | לא נדרש |
| Selectors | לא (כל שינוי מרנדר הכל) | כן (רנדר רק מה שהשתנה) |
| Middleware | אין | persist, devtools, immer |
| שימוש מחוץ לריאקט | לא | כן |
| Boilerplate | הרבה (context, provider, reducer, types) | מעט |
| מתאים ל | state פשוט, ערכת נושא, שפה | state מורכב, אפליקציות גדולות |
טיפים ושיטות עבודה מומלצות¶
שימוש ב-selectors ממוקדים¶
// לא מומלץ - מחזיר את כל ה-store
const store = useStore();
// מומלץ - בוחר רק מה שצריך
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
// אפשר גם לבחור מספר שדות
const { count, increment } = useStore((state) => ({
count: state.count,
increment: state.increment,
}));
גישה ל-store מחוץ לריאקט¶
// קריאה
const currentCount = useCounterStore.getState().count;
// עדכון
useCounterStore.getState().increment();
// האזנה לשינויים
const unsubscribe = useCounterStore.subscribe((state) => {
console.log("State changed:", state.count);
});
סיכום¶
- Zustand היא ספריית ניהול state קלה, פשוטה וחזקה
- יצירת store עם create, שימוש עם selector בקומפוננטות
- לא צריך Provider - ה-store זמין בכל מקום
- רנדר חכם - קומפוננטה מתעדכנת רק כשהנתון שהיא מאזינה לו משתנה
- Middleware מובנה: persist לשמירה, devtools לדיבאג, immer לעדכונים קלים
- Slices מאפשרים פיצול store גדול לחלקים מנוהלים
- API פשוט עם מעט boilerplate לעומת Redux