לדלג לתוכן

7.7 ניהול סטייט Zustand פתרון

פתרון - ניהול סטייט - Zustand


פתרון תרגיל 1 - Store בסיסי - רשימת משימות

import { create } from "zustand";

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

type Filter = "all" | "active" | "completed";

interface TodoStore {
  todos: Todo[];
  filter: Filter;
  addTodo: (text: string) => void;
  toggleTodo: (id: number) => void;
  removeTodo: (id: number) => void;
  editTodo: (id: number, text: string) => void;
  setFilter: (filter: Filter) => void;
  clearCompleted: () => void;
  getFilteredTodos: () => Todo[];
  getStats: () => { total: number; active: number; completed: number };
}

const useTodoStore = create<TodoStore>((set, get) => ({
  todos: [],
  filter: "all",

  addTodo: (text) =>
    set((state) => ({
      todos: [...state.todos, { id: Date.now(), text, completed: false }],
    })),

  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((t) =>
        t.id === id ? { ...t, completed: !t.completed } : t
      ),
    })),

  removeTodo: (id) =>
    set((state) => ({
      todos: state.todos.filter((t) => t.id !== id),
    })),

  editTodo: (id, text) =>
    set((state) => ({
      todos: state.todos.map((t) => (t.id === id ? { ...t, text } : t)),
    })),

  setFilter: (filter) => set({ filter }),

  clearCompleted: () =>
    set((state) => ({
      todos: state.todos.filter((t) => !t.completed),
    })),

  getFilteredTodos: () => {
    const { todos, filter } = get();
    switch (filter) {
      case "active":
        return todos.filter((t) => !t.completed);
      case "completed":
        return todos.filter((t) => t.completed);
      default:
        return todos;
    }
  },

  getStats: () => {
    const { todos } = get();
    const completed = todos.filter((t) => t.completed).length;
    return {
      total: todos.length,
      active: todos.length - completed,
      completed,
    };
  },
}));

// קומפוננטות
function TodoInput() {
  const addTodo = useTodoStore((s) => s.addTodo);
  const [text, setText] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (text.trim()) {
      addTodo(text.trim());
      setText("");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={(e) => setText(e.target.value)} placeholder="משימה חדשה..." />
      <button type="submit">הוסף</button>
    </form>
  );
}

function TodoItem({ todo }: { todo: Todo }) {
  const toggleTodo = useTodoStore((s) => s.toggleTodo);
  const removeTodo = useTodoStore((s) => s.removeTodo);

  return (
    <li>
      <span
        onClick={() => toggleTodo(todo.id)}
        style={{
          textDecoration: todo.completed ? "line-through" : "none",
          cursor: "pointer",
        }}
      >
        {todo.text}
      </span>
      <button onClick={() => removeTodo(todo.id)}>מחק</button>
    </li>
  );
}

function TodoList() {
  const getFilteredTodos = useTodoStore((s) => s.getFilteredTodos);
  const filteredTodos = getFilteredTodos();

  return (
    <ul>
      {filteredTodos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

function TodoFilters() {
  const filter = useTodoStore((s) => s.filter);
  const setFilter = useTodoStore((s) => s.setFilter);
  const clearCompleted = useTodoStore((s) => s.clearCompleted);

  return (
    <div>
      {(["all", "active", "completed"] as Filter[]).map((f) => (
        <button
          key={f}
          onClick={() => setFilter(f)}
          style={{ fontWeight: filter === f ? "bold" : "normal" }}
        >
          {f === "all" ? "הכל" : f === "active" ? "פעילות" : "הושלמו"}
        </button>
      ))}
      <button onClick={clearCompleted}>נקה שהושלמו</button>
    </div>
  );
}

function TodoStats() {
  const getStats = useTodoStore((s) => s.getStats);
  const stats = getStats();

  return (
    <p>
      סה"כ: {stats.total} | פעילות: {stats.active} | הושלמו: {stats.completed}
    </p>
  );
}

הסבר:
- כל קומפוננטה בוחרת רק את מה שהיא צריכה מה-store
- TodoStats ו-TodoFilters לא מרנדרים מחדש כשמוסיפים משימה (הם לא מאזינים ל-todos ישירות)
- getFilteredTodos ו-getStats משתמשים ב-get() לגישה ל-state הנוכחי


פתרון תרגיל 2 - Store עם Persist

import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

interface Note {
  id: string;
  title: string;
  content: string;
  color: string;
  createdAt: string;
  updatedAt: string;
}

type SortBy = "createdAt" | "updatedAt";
type ViewMode = "grid" | "list";

interface NotesStore {
  notes: Note[];
  searchQuery: string;
  sortBy: SortBy;
  viewMode: ViewMode;

  createNote: (title: string, content: string, color?: string) => void;
  updateNote: (id: string, updates: Partial<Pick<Note, "title" | "content" | "color">>) => void;
  deleteNote: (id: string) => void;
  setSearchQuery: (query: string) => void;
  setSortBy: (sort: SortBy) => void;
  setViewMode: (mode: ViewMode) => void;
  getFilteredNotes: () => Note[];
}

const useNotesStore = create<NotesStore>()(
  persist(
    (set, get) => ({
      notes: [],
      searchQuery: "",
      sortBy: "updatedAt" as SortBy,
      viewMode: "grid" as ViewMode,

      createNote: (title, content, color = "#ffffff") =>
        set((state) => ({
          notes: [
            {
              id: Date.now().toString(),
              title,
              content,
              color,
              createdAt: new Date().toISOString(),
              updatedAt: new Date().toISOString(),
            },
            ...state.notes,
          ],
        })),

      updateNote: (id, updates) =>
        set((state) => ({
          notes: state.notes.map((note) =>
            note.id === id
              ? { ...note, ...updates, updatedAt: new Date().toISOString() }
              : note
          ),
        })),

      deleteNote: (id) =>
        set((state) => ({
          notes: state.notes.filter((n) => n.id !== id),
        })),

      setSearchQuery: (query) => set({ searchQuery: query }),
      setSortBy: (sort) => set({ sortBy: sort }),
      setViewMode: (mode) => set({ viewMode: mode }),

      getFilteredNotes: () => {
        const { notes, searchQuery, sortBy } = get();
        let filtered = notes;

        if (searchQuery) {
          const q = searchQuery.toLowerCase();
          filtered = filtered.filter(
            (n) =>
              n.title.toLowerCase().includes(q) ||
              n.content.toLowerCase().includes(q)
          );
        }

        return filtered.sort(
          (a, b) => new Date(b[sortBy]).getTime() - new Date(a[sortBy]).getTime()
        );
      },
    }),
    {
      name: "notes-storage",
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({
        notes: state.notes,
        viewMode: state.viewMode,
        sortBy: state.sortBy,
      }),
    }
  )
);

// קומפוננטות
function NoteCard({ note }: { note: Note }) {
  const updateNote = useNotesStore((s) => s.updateNote);
  const deleteNote = useNotesStore((s) => s.deleteNote);

  const colors = ["#ffffff", "#fff3cd", "#d1ecf1", "#d4edda", "#f8d7da", "#e2d5f1"];

  return (
    <div
      style={{
        backgroundColor: note.color,
        padding: "16px",
        borderRadius: "8px",
        border: "1px solid #ddd",
      }}
    >
      <h3>{note.title}</h3>
      <p>{note.content}</p>
      <p style={{ fontSize: "12px", color: "#666" }}>
        עודכן: {new Date(note.updatedAt).toLocaleDateString("he-IL")}
      </p>
      <div>
        {colors.map((c) => (
          <button
            key={c}
            onClick={() => updateNote(note.id, { color: c })}
            style={{
              width: "20px",
              height: "20px",
              backgroundColor: c,
              border: c === note.color ? "2px solid #333" : "1px solid #ccc",
              borderRadius: "50%",
              cursor: "pointer",
            }}
          />
        ))}
        <button onClick={() => deleteNote(note.id)}>מחק</button>
      </div>
    </div>
  );
}

function NotesApp() {
  const searchQuery = useNotesStore((s) => s.searchQuery);
  const setSearchQuery = useNotesStore((s) => s.setSearchQuery);
  const viewMode = useNotesStore((s) => s.viewMode);
  const setViewMode = useNotesStore((s) => s.setViewMode);
  const sortBy = useNotesStore((s) => s.sortBy);
  const setSortBy = useNotesStore((s) => s.setSortBy);
  const createNote = useNotesStore((s) => s.createNote);
  const getFilteredNotes = useNotesStore((s) => s.getFilteredNotes);

  const notes = getFilteredNotes();

  return (
    <div>
      <h1>ההערות שלי</h1>
      <div>
        <input
          value={searchQuery}
          onChange={(e) => setSearchQuery(e.target.value)}
          placeholder="חפש..."
        />
        <button onClick={() => setViewMode(viewMode === "grid" ? "list" : "grid")}>
          {viewMode === "grid" ? "תצוגת רשימה" : "תצוגת Grid"}
        </button>
        <select value={sortBy} onChange={(e) => setSortBy(e.target.value as SortBy)}>
          <option value="updatedAt">לפי עדכון</option>
          <option value="createdAt">לפי יצירה</option>
        </select>
        <button onClick={() => createNote("הערה חדשה", "")}>הערה חדשה</button>
      </div>
      <div
        style={{
          display: viewMode === "grid" ? "grid" : "flex",
          gridTemplateColumns: "repeat(3, 1fr)",
          flexDirection: "column",
          gap: "16px",
        }}
      >
        {notes.map((note) => (
          <NoteCard key={note.id} note={note} />
        ))}
      </div>
    </div>
  );
}

הסבר:
- persist שומר אוטומטית ב-localStorage - ההערות שמורות בין רענונים
- partialize מפסיק משמירת searchQuery (לא הגיוני לשמור חיפוש בין סשנים)
- צבעי ההערות ניתנים לשינוי דרך כפתורי צבע


פתרון תרגיל 3 - Store עם Immer

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

interface Task {
  id: string;
  title: string;
  description: string;
}

interface Column {
  id: string;
  name: string;
  tasks: Task[];
}

interface Board {
  id: string;
  name: string;
  columns: Column[];
}

interface Project {
  id: string;
  name: string;
  boards: Board[];
}

interface ProjectStore {
  projects: Project[];
  addProject: (name: string) => void;
  removeProject: (projectId: string) => void;
  addBoard: (projectId: string, name: string) => void;
  removeBoard: (projectId: string, boardId: string) => void;
  addTask: (projectId: string, boardId: string, columnId: string, task: Omit<Task, "id">) => void;
  moveTask: (
    projectId: string,
    boardId: string,
    fromColumnId: string,
    toColumnId: string,
    taskId: string
  ) => void;
  editTask: (projectId: string, boardId: string, columnId: string, taskId: string, updates: Partial<Task>) => void;
  removeTask: (projectId: string, boardId: string, columnId: string, taskId: string) => void;
}

const useProjectStore = create<ProjectStore>()(
  immer((set) => ({
    projects: [],

    addProject: (name) =>
      set((state) => {
        state.projects.push({
          id: Date.now().toString(),
          name,
          boards: [],
        });
      }),

    removeProject: (projectId) =>
      set((state) => {
        const index = state.projects.findIndex((p) => p.id === projectId);
        if (index !== -1) state.projects.splice(index, 1);
      }),

    addBoard: (projectId, name) =>
      set((state) => {
        const project = state.projects.find((p) => p.id === projectId);
        if (project) {
          project.boards.push({
            id: Date.now().toString(),
            name,
            columns: [
              { id: "todo", name: "לביצוע", tasks: [] },
              { id: "progress", name: "בתהליך", tasks: [] },
              { id: "done", name: "הושלם", tasks: [] },
            ],
          });
        }
      }),

    removeBoard: (projectId, boardId) =>
      set((state) => {
        const project = state.projects.find((p) => p.id === projectId);
        if (project) {
          const index = project.boards.findIndex((b) => b.id === boardId);
          if (index !== -1) project.boards.splice(index, 1);
        }
      }),

    addTask: (projectId, boardId, columnId, task) =>
      set((state) => {
        const project = state.projects.find((p) => p.id === projectId);
        const board = project?.boards.find((b) => b.id === boardId);
        const column = board?.columns.find((c) => c.id === columnId);
        if (column) {
          column.tasks.push({ ...task, id: Date.now().toString() });
        }
      }),

    moveTask: (projectId, boardId, fromColumnId, toColumnId, taskId) =>
      set((state) => {
        const project = state.projects.find((p) => p.id === projectId);
        const board = project?.boards.find((b) => b.id === boardId);
        if (!board) return;

        const fromColumn = board.columns.find((c) => c.id === fromColumnId);
        const toColumn = board.columns.find((c) => c.id === toColumnId);
        if (!fromColumn || !toColumn) return;

        const taskIndex = fromColumn.tasks.findIndex((t) => t.id === taskId);
        if (taskIndex === -1) return;

        const [task] = fromColumn.tasks.splice(taskIndex, 1);
        toColumn.tasks.push(task);
      }),

    editTask: (projectId, boardId, columnId, taskId, updates) =>
      set((state) => {
        const project = state.projects.find((p) => p.id === projectId);
        const board = project?.boards.find((b) => b.id === boardId);
        const column = board?.columns.find((c) => c.id === columnId);
        const task = column?.tasks.find((t) => t.id === taskId);
        if (task) {
          Object.assign(task, updates);
        }
      }),

    removeTask: (projectId, boardId, columnId, taskId) =>
      set((state) => {
        const project = state.projects.find((p) => p.id === projectId);
        const board = project?.boards.find((b) => b.id === boardId);
        const column = board?.columns.find((c) => c.id === columnId);
        if (column) {
          const index = column.tasks.findIndex((t) => t.id === taskId);
          if (index !== -1) column.tasks.splice(index, 1);
        }
      }),
  }))
);

הסبر:
- בלי immer, moveTask היה דורש שכפול של כל רמות ה-state (project -> board -> columns)
- עם immer, אנחנו פשוט מוצאים את האלמנט ומשנים אותו ישירות
- splice, push, ו-Object.assign עובדים ישירות על ה-draft - immer מבצע את ה-immutable update מאחורי הקלעים


פתרון תרגיל 4 - Store עם Slices

import { create, StateCreator } from "zustand";
import { devtools } from "zustand/middleware";

// Auth Slice
interface AuthSlice {
  user: { id: string; name: string; email: string } | null;
  token: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  isAuthenticated: () => boolean;
}

type StoreState = AuthSlice & CartSlice & ProductsSlice & UISlice;

const createAuthSlice: StateCreator<StoreState, ["zustand/devtools", never](<../../../"zustand/devtools", never.md>), [], AuthSlice> = (set, get) => ({
  user: null,
  token: null,
  login: async (email, password) => {
    await new Promise((r) => setTimeout(r, 1000));
    set(
      {
        user: { id: "1", name: "משתמש", email },
        token: "fake-token",
      },
      false,
      "auth/login"
    );
  },
  logout: () => {
    set(
      { user: null, token: null, cart: [] },
      false,
      "auth/logout"
    );
  },
  isAuthenticated: () => get().token !== null,
});

// Cart Slice
interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

interface CartSlice {
  cart: CartItem[];
  addItem: (product: { id: number; name: string; price: number }) => void;
  removeItem: (id: number) => void;
  updateQuantity: (id: number, quantity: number) => void;
  getTotal: () => number;
  clearCart: () => void;
}

const createCartSlice: StateCreator<StoreState, ["zustand/devtools", never](<../../../"zustand/devtools", never.md>), [], CartSlice> = (set, get) => ({
  cart: [],
  addItem: (product) =>
    set(
      (state) => {
        const existing = state.cart.find((i) => i.id === product.id);
        if (existing) {
          return {
            cart: state.cart.map((i) =>
              i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
            ),
          };
        }
        return { cart: [...state.cart, { ...product, quantity: 1 }] };
      },
      false,
      "cart/addItem"
    ),
  removeItem: (id) =>
    set(
      (state) => ({ cart: state.cart.filter((i) => i.id !== id) }),
      false,
      "cart/removeItem"
    ),
  updateQuantity: (id, quantity) =>
    set(
      (state) => ({
        cart: quantity <= 0
          ? state.cart.filter((i) => i.id !== id)
          : state.cart.map((i) => (i.id === id ? { ...i, quantity } : i)),
      }),
      false,
      "cart/updateQuantity"
    ),
  getTotal: () => get().cart.reduce((sum, i) => sum + i.price * i.quantity, 0),
  clearCart: () => set({ cart: [] }, false, "cart/clear"),
});

// Products Slice
interface ProductsSlice {
  products: { id: number; name: string; price: number }[];
  productsLoading: boolean;
  productsError: string | null;
  fetchProducts: () => Promise<void>;
}

const createProductsSlice: StateCreator<StoreState, ["zustand/devtools", never](<../../../"zustand/devtools", never.md>), [], ProductsSlice> = (set) => ({
  products: [],
  productsLoading: false,
  productsError: null,
  fetchProducts: async () => {
    set({ productsLoading: true, productsError: null }, false, "products/fetchStart");
    try {
      await new Promise((r) => setTimeout(r, 1000));
      const products = [
        { id: 1, name: "טלפון", price: 2999 },
        { id: 2, name: "אוזניות", price: 499 },
        { id: 3, name: "מטען", price: 99 },
      ];
      set({ products, productsLoading: false }, false, "products/fetchSuccess");
    } catch {
      set({ productsError: "שגיאה בטעינה", productsLoading: false }, false, "products/fetchError");
    }
  },
});

// UI Slice
interface UISlice {
  theme: "light" | "dark";
  sidebarOpen: boolean;
  toggleTheme: () => void;
  toggleSidebar: () => void;
}

const createUISlice: StateCreator<StoreState, ["zustand/devtools", never](<../../../"zustand/devtools", never.md>), [], UISlice> = (set) => ({
  theme: "light",
  sidebarOpen: true,
  toggleTheme: () =>
    set(
      (state) => ({ theme: state.theme === "light" ? "dark" : "light" }),
      false,
      "ui/toggleTheme"
    ),
  toggleSidebar: () =>
    set(
      (state) => ({ sidebarOpen: !state.sidebarOpen }),
      false,
      "ui/toggleSidebar"
    ),
});

// חיבור
const useStore = create<StoreState>()(
  devtools(
    (...args) => ({
      ...createAuthSlice(...args),
      ...createCartSlice(...args),
      ...createProductsSlice(...args),
      ...createUISlice(...args),
    }),
    { name: "AppStore" }
  )
);

הסבר:
- כל slice מוגדר בנפרד עם StateCreator
- כל action מקבל שם (הפרמטר השלישי של set) שמוצג ב-Redux DevTools
- logout מנקה גם user וגם cart - דוגמה לאינטראקציה בין slices
- devtools עוטף את כל ה-store ומאפשר דיבאג עם Redux DevTools


פתרון תרגיל 5 - Store עם DevTools ובדיקות

// store.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";

interface Task {
  id: number;
  text: string;
  done: boolean;
}

interface TaskStore {
  tasks: Task[];
  addTask: (text: string) => void;
  toggleTask: (id: number) => void;
  removeTask: (id: number) => void;
  getActiveTasks: () => Task[];
  getDoneTasks: () => Task[];
}

export const useTaskStore = create<TaskStore>()(
  devtools(
    (set, get) => ({
      tasks: [],
      addTask: (text) =>
        set(
          (state) => ({
            tasks: [...state.tasks, { id: Date.now(), text, done: false }],
          }),
          false,
          "tasks/add"
        ),
      toggleTask: (id) =>
        set(
          (state) => ({
            tasks: state.tasks.map((t) =>
              t.id === id ? { ...t, done: !t.done } : t
            ),
          }),
          false,
          "tasks/toggle"
        ),
      removeTask: (id) =>
        set(
          (state) => ({ tasks: state.tasks.filter((t) => t.id !== id) }),
          false,
          "tasks/remove"
        ),
      getActiveTasks: () => get().tasks.filter((t) => !t.done),
      getDoneTasks: () => get().tasks.filter((t) => t.done),
    }),
    { name: "TaskStore" }
  )
);

// store.test.ts
import { useTaskStore } from "./store";

describe("TaskStore", () => {
  beforeEach(() => {
    // איפוס ה-store לפני כל בדיקה
    useTaskStore.setState({ tasks: [] });
  });

  it("should start with empty tasks", () => {
    const state = useTaskStore.getState();
    expect(state.tasks).toEqual([]);
  });

  it("should add a task", () => {
    useTaskStore.getState().addTask("משימה חדשה");
    const state = useTaskStore.getState();
    expect(state.tasks).toHaveLength(1);
    expect(state.tasks[0].text).toBe("משימה חדשה");
    expect(state.tasks[0].done).toBe(false);
  });

  it("should toggle a task", () => {
    useTaskStore.getState().addTask("משימה");
    const taskId = useTaskStore.getState().tasks[0].id;

    useTaskStore.getState().toggleTask(taskId);
    expect(useTaskStore.getState().tasks[0].done).toBe(true);

    useTaskStore.getState().toggleTask(taskId);
    expect(useTaskStore.getState().tasks[0].done).toBe(false);
  });

  it("should remove a task", () => {
    useTaskStore.getState().addTask("משימה 1");
    useTaskStore.getState().addTask("משימה 2");
    const taskId = useTaskStore.getState().tasks[0].id;

    useTaskStore.getState().removeTask(taskId);
    expect(useTaskStore.getState().tasks).toHaveLength(1);
    expect(useTaskStore.getState().tasks[0].text).toBe("משימה 2");
  });

  it("should return active tasks", () => {
    useTaskStore.getState().addTask("פעילה");
    useTaskStore.getState().addTask("הושלמה");
    const doneId = useTaskStore.getState().tasks[1].id;
    useTaskStore.getState().toggleTask(doneId);

    const active = useTaskStore.getState().getActiveTasks();
    expect(active).toHaveLength(1);
    expect(active[0].text).toBe("פעילה");
  });

  it("should return done tasks", () => {
    useTaskStore.getState().addTask("פעילה");
    useTaskStore.getState().addTask("הושלמה");
    const doneId = useTaskStore.getState().tasks[1].id;
    useTaskStore.getState().toggleTask(doneId);

    const done = useTaskStore.getState().getDoneTasks();
    expect(done).toHaveLength(1);
    expect(done[0].text).toBe("הושלמה");
  });
});

הסبر:
- בדיקות Zustand פשוטות מאוד - לא צריך רנדר, פשוט קוראים ל-getState ולפעולות
- setState מאפשר איפוס ה-store בין בדיקות
- כל action מקבל שם שמוצג ב-Redux DevTools


תשובות לשאלות

  1. יתרון selectors: ב-useContext, כל שינוי בכל חלק של ה-state מרנדר את כל הקומפוננטות שצורכות את הקונטקסט. ב-Zustand, selector מאפשר לקומפוננטה לבחור בדיוק מה היא צריכה, ולרנדר מחדש רק כשהנתון הספציפי הזה משתנה. זה הבדל ביצועים משמעותי באפליקציות גדולות.

  2. למה אין Provider: Zustand יוצר store כמשתנה גלובלי (module-level). ה-store הוא singleton שקיים מחוץ לעץ הריאקט. ההוק useStore מתחבר ל-store הגלובלי ומאזין לשינויים. זה אומר שגם קוד לא-ריאקטי יכול לגשת ל-store דרך getState.

  3. set עם אובייקט מול פונקציה: set({ count: 5 }) מגדיר ערך קבוע (merge רדוד). set(state => ({ count: state.count + 1 })) משתמש ב-state הנוכחי לחישוב הערך החדש. צריך פונקציה כשהערך החדש תלוי בערך הנוכחי (כמו increment), ואובייקט כשקובעים ערך מוחלט (כמו reset).

  4. מתי slices: כדאי לפצל כשה-store גדול ויש חלקים עצמאיים ברורים (auth, cart, ui). store אחד עדיף כשכל ה-state קשור (כמו store של טופס אחד) או כשהאפליקציה קטנה. כלל אצבע: אם ה-store מכיל יותר מ-15-20 שדות, כדאי לשקול פיצול.

  5. state אסינכרוני: Zustand מטפל ב-async פשוט - הפעולות ב-store יכולות להיות async. קוראים ל-set מספר פעמים: פעם אחת לסימון loading, פעם שנייה לעדכון הנתונים אחרי ההצלחה, ופעם שלישית לשגיאה אם נכשל. אין צורך ב-middleware מיוחד כמו thunks ב-Redux.