הבחירה שמשפיעה על כל render, State Management ב-React Native
אפליקציה עם עשרות אלפי משתמשים לא נכשלת בגלל עיצוב, היא נכשלת בגלל state שמסתדס. רינדרים מיותרים שמאיטים UI, נתונים מיושנים שמוצגים למשתמש, race conditions שמשחיתים data, כולם תוצאה ישירה של ארכיטקטורת state שגויה. ב-2026 הכלים הנכונים ברורים, אבל הבחירה ביניהם עדיין מבלבלת. השיעור הזה מכניס סדר.
נקודת הפתיחה: ב-React Native יש שני סוגי state שצריך לנהל בצורה שונה לחלוטין. Server state, נתונים שמגיעים מ-API: מוצרים, הזמנות, פרופיל משתמש. Client state, מצב ה-UI: האם menu פתוח, איזה tab נבחר, מה הועבר בחיפוש. הטעות הנפוצה ביותר היא לנהל את שניהם עם אותו כלי. כשמנהלים server state ב-Zustand, מקבלים caching ידני שתמיד יהיה פגום. כשמנהלים client state ב-Redux, מקבלים boilerplate ענק בלי שום ערך מוסף.
ארבעת הכלים, מה כל אחד עושה ומתי
| כלי | גודל | מתאים ל | לא מתאים ל |
|---|---|---|---|
| Zustand | ~1KB | Client state גלובלי: cart, auth, UI preferences | Server state, אין caching מובנה |
| TanStack Query | ~13KB | Server state: כל נתון שמגיע מ-API | UI state שלא נגיע ל-server |
| Jotai | ~2.5KB | State אטומי עם interdependencies מורכבות | Large stores עם הרבה actions |
| Redux Toolkit | ~15KB | Enterprise apps, teams גדולים, מורשת | Apps קטנים-בינוניים, overkill |
כפי שמסכם dev.to: "In many apps, a hybrid approach works best: use Zustand for UI/local state and React Query for server state." זו המלצת ה-default לכל אפליקציה חדשה ב-2026.
Prompt לבחירת ארכיטקטורה, Claude כ-Architect
הפרומפט הזה מניח שClaude מבין את ה-scale שלכם ונותן recommendation שמותאם לכם, לא תשובה גנרית:
כל שורה ב-prompt החזק עושה עבודה: expertise framing מושך ידע ארכיטקטורי לא רק syntax. scale משפיע על cache strategy. team size משפיע על learning curve. concrete example מונע תשובה abstract.
Zustand v5, מה השתנה ומה צריך לדעת
Zustand v5 יצא ב-Q4 2024 עם שינויים שבירים. אם אתם ב-v4, שימו לב:
- React 18 נדרש. אין support לReact < 18. React Native 0.73+ כולל React 18 ומעלה.
- Object selector גורם ל-infinite loop.
useStore(s => ({ a: s.a, b: s.b }))יוצר reference חדשה בכל render. עברו ל-useShallowמ-zustand/react/shallow. - Persist behavioral change. state initial אינו נשמר אוטומטית יותר. הגדירו state ב-store בנפרד.
- Custom equality:
createלא תומך ב-equality function ישיר. השתמשו ב-createWithEqualityFn.
דוגמה לstore נכון עם Zustand v5 ו-MMKV persistence:
// store/cartStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { useShallow } from 'zustand/react/shallow'; // v5, חובה לobject selectors
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
const mmkvStorage = {
getItem: (key: string) => storage.getString(key) ?? null,
setItem: (key: string, value: string) => storage.set(key, value),
removeItem: (key: string) => storage.delete(key),
};
type CartItem = {
productId: string;
name: string;
price: number;
quantity: number;
inStock: boolean;
};
type CartStore = {
items: CartItem[];
promoCode: string | null;
discount: number;
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (productId: string) => void;
updateQuantity: (productId: string, qty: number) => void;
applyPromoCode: (code: string, pct: number) => void;
clearCart: () => void;
total: () => number;
};
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
promoCode: null,
discount: 0,
addItem: (item) => set((s) => {
const existing = s.items.find(i => i.productId === item.productId);
if (existing) {
return { items: s.items.map(i =>
i.productId === item.productId ? { ...i, quantity: i.quantity + 1 } : i
)};
}
return { items: [...s.items, { ...item, quantity: 1 }] };
}),
removeItem: (productId) => set((s) => ({
items: s.items.filter(i => i.productId !== productId)
})),
updateQuantity: (productId, qty) => {
if (qty <= 0) { get().removeItem(productId); return; }
set((s) => ({ items: s.items.map(i =>
i.productId === productId ? { ...i, quantity: qty } : i
)}));
},
applyPromoCode: (code, pct) => set({ promoCode: code, discount: pct }),
clearCart: () => set({ items: [], promoCode: null, discount: 0 }),
total: () => {
const { items, discount } = get();
const sub = items.filter(i => i.inStock)
.reduce((sum, i) => sum + i.price * i.quantity, 0);
return sub * (1 - discount / 100);
},
}),
{
name: 'cart-storage',
storage: createJSONStorage(() => mmkvStorage),
version: 1, // חובה, מאפשר migration בעתיד
}
)
);
// Hook שמסתיר את ה-store internals
// שימוש ב-selectors נפרדים, לא object אחד גדול
export const useCart = () => ({
items: useCartStore(s => s.items),
total: useCartStore(s => s.total()),
itemCount: useCartStore(s => s.items.reduce((n, i) => n + i.quantity, 0)),
addItem: useCartStore(s => s.addItem),
removeItem: useCartStore(s => s.removeItem),
clearCart: useCartStore(s => s.clearCart),
});TanStack Query ב-React Native, Offline ו-WebSocket
React Query לא מתנהג כמו בweb בתוך React Native. שני דברים חשובים שרוב מדריכים לא מזכירים:
Network reconnect אינו אוטומטי ב-mobile. ב-browser React Query מזהה reconnect לרשת ומבצע refetch. ב-React Native צריך להוסיף ידנית:
import NetInfo from '@react-native-community/netinfo';
import { onlineManager } from '@tanstack/react-query';
onlineManager.setEventListener(setOnline => {
return NetInfo.addEventListener(state => {
setOnline(!!state.isConnected);
});
});WebSocket + React Query: במקום לנהל WebSocket state ידנית ב-Zustand, כשמגיע event בWebSocket, קראו ל-queryClient.invalidateQueries(['orders']). React Query יבצע refetch חכם ויעדכן את כל ה-components שsubscribed לquery. זה מפחית bugs ב-~80% לעומת ניהול state WebSocket ידנית ב-Redux.
MMKV, למה לא AsyncStorage
MMKV הוא C++ native module שעושה memory-mapped files. הוא עד 30x מהיר מAsyncStorage. ב-app ישראלי של סטארטאפ שמגיש 50K sessions ביום, ה-session restore מ-MMKV לוקח 3-5ms לעומת 80-150ms מAsyncStorage. ההבדל מורגש בפתיחת האפליקציה.
MMKV גם synchronous, אין צורך ב-async/await לread operations. זה מפשט קוד רב, בעיקר ב-store initializers וב-hooks שנקראים ב-render.
טעויות נפוצות
- ניהול server state ב-Zustand: אם state מגיע מ-API, תנו לTanStack Query לנהל אותו. Zustand לא יודע מתי הנתון מיושן, לא עושה background refetch, ולא מטפל בerror retry. התוצאה: נתונים ישנים שמוצגים למשתמש.
- Selector שמחזיר object ב-Zustand v5:
useCartStore(s => ({ items: s.items, total: s.total() }))יוצר object חדש בכל render. ב-v5 זה גורם ל-infinite loop. פצלו לשני selectors נפרדים או השתמשו ב-useShallow. - Persist ללא version: כשמשנים מבנה ה-store, שם field, הוספת field, הסרת field, persist פוגש data ישן ב-storage. הוסיפו
version: 1ו-migratefunction מהיום הראשון. שינוי זה בzustand v5 שבר apps רבים שלא הכינו migrate.
סיכום, ארכיטקטורת state ב-2026
- Server state (API data): TanStack Query. תמיד. כולל offline persistence עם PersistQueryClientProvider.
- Client state (UI): Zustand לרוב האפליקציות. Jotai כשיש atomic dependencies מורכבות.
- Persistence: MMKV בכל מקרה שperformance חשוב. AsyncStorage רק אם לא רוצים native dependency.
- Zustand v5:
useShallowלכל object selector.versionו-migrateב-persist מהיום הראשון. - Claude כarchitect: תנו context מלא, scale, team, offline requirements. תקבלו recommendation שמותאם לכם ולא תשובה גנרית.
