مقدمه
اگه یه چیزی باشه که خیلی از توسعهدهندههای React Native باهاش دست و پنجه نرم کردن، اون مدیریت State هست. صادقانه بگم، انتخاب ابزار درست برای این کار میتونه تاثیر خیلی جدی روی عملکرد اپ، نگهداری کد و کلاً تجربه روزانهتون به عنوان توسعهدهنده داشته باشه.
خب، سال ۲۰۲۶ رسیدیم و اکوسیستم مدیریت State به شکل قابل توجهی تکامل پیدا کرده. رویکرد غالب الان به سمت ترکیب هوشمندانه ابزارهای تخصصی رفته — به جای اینکه یک ابزار واحد همه کارها رو انجام بده.
یادتون هست که Redux تقریباً تنها گزینه جدی بود؟ اون دوران گذشته. امروز کتابخانههایی مثل Zustand، Jotai و TanStack Query هر کدوم یه بخش مشخص از مسئله رو به بهترین شکل حل میکنن. Zustand با بیش از ۳۰ درصد رشد سالانه، تقریباً در ۴۰ درصد پروژههای جدید استفاده میشه و TanStack Query حدود ۸۰ درصد الگوهای مدیریت داده سمت سرور رو پوشش میده.
تو این راهنما، اول مفهوم تفکیک State سمت کلاینت و سرور رو بررسی میکنیم. بعد هر سه کتابخانه رو با نمونه کدهای واقعی آموزش میدیم و در نهایت نشون میدیم چطور اینا رو کنار هم برای ساخت اپلیکیشنهای حرفهای به کار بگیرید.
تفکیک State سمت کلاینت و State سمت سرور: تغییر پارادایم
مهمترین تحولی که تو مدیریت State طی سالهای اخیر اتفاق افتاده، درک این نکته بوده که تمام Stateها یکسان نیستن. خب بذارید واضحتر بگم — وضعیت اپلیکیشن به دو دسته اصلی تقسیم میشه که هر کدوم ویژگیها و نیازهای کاملاً متفاوتی دارن.
State سمت کلاینت (Client State)
این نوع State کاملاً تحت کنترل خود اپلیکیشنه و شامل موارد زیر میشه:
- وضعیت رابط کاربری: آیا منوی کناری بازه؟ کدوم تب فعاله؟ آیا مدال نمایش داده میشه؟
- ترجیحات کاربر: تم تاریک/روشن، زبان انتخابی، اندازه فونت
- دادههای فرم: مقادیر ورودیها قبل از ارسال به سرور
- وضعیت ناوبری: صفحه فعلی، تاریخچه ناوبری
- وضعیت احراز هویت: توکن دسترسی، اطلاعات کاربر لاگینشده
State سمت سرور (Server State)
این نوع State منبع حقیقتش تو سرور قرار داره و چالشهای خاص خودش رو داره:
- دادههای دریافتی از API: لیست محصولات، پروفایل کاربران، پستهای وبلاگ
- کشینگ: چه زمانی دادهها قدیمی (stale) میشن و باید دوباره واکشی بشن؟
- همگامسازی: تطبیق دادههای محلی با تغییرات سمت سرور
- صفحهبندی: بارگذاری تدریجی دادهها تو لیستهای بلند
- بهروزرسانی خوشبینانه: نمایش فوری تغییرات قبل از تایید سرور
یه اشتباه خیلی رایج که من خودم هم اوایل مرتکبش شدم: ذخیره پاسخهای API تو Redux یا Zustand به عنوان State عمومی. این کار منجر به کد تکراری، مدیریت دستی کشینگ و یه عالمه مشکلات همگامسازی میشه. راهحل مدرن اینه:
- Zustand یا Jotai برای State سمت کلاینت — ترجیحات کاربر، وضعیت UI، دادههای فرم
- TanStack Query برای State سمت سرور — واکشی داده، کشینگ، همگامسازی
Zustand: مدیریت State ساده و قدرتمند
Zustand (به آلمانی یعنی «وضعیت») یه کتابخانه سبک و مینیمال برای مدیریت State هست که توسط تیم pmndrs توسعه داده شده. حجم باندلش فقط حدود ۱ کیلوبایته و API فوقالعاده سادهای داره.
اگه از بویلرپلیت زیاد Redux خسته شدید (و صادقانه، کی نشده؟)، Zustand بهترین جایگزینه.
نصب و راهاندازی
npm install zustand
# یا
yarn add zustand
ساخت اولین Store
ساخت Store تو Zustand فوقالعاده سادهست. کافیه یه تابع به create بدید:
// stores/useAuthStore.ts
import { create } from 'zustand';
interface User {
id: string;
name: string;
email: string;
avatarUrl: string;
}
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
login: (user: User, token: string) => void;
logout: () => void;
updateProfile: (updates: Partial<User>) => void;
}
const useAuthStore = create<AuthState>()((set) => ({
user: null,
token: null,
isAuthenticated: false,
login: (user, token) =>
set({
user,
token,
isAuthenticated: true,
}),
logout: () =>
set({
user: null,
token: null,
isAuthenticated: false,
}),
updateProfile: (updates) =>
set((state) => ({
user: state.user
? { ...state.user, ...updates }
: null,
})),
}))
export default useAuthStore;
استفاده در کامپوننتها
استفاده از Store تو کامپوننتها خیلی شبیه به هوکهای معمولی React هست — و این دقیقاً همون چیزیه که آدم دوست داره:
// components/ProfileScreen.tsx
import React from 'react';
import { View, Text, Image, TouchableOpacity } from 'react-native';
import useAuthStore from '../stores/useAuthStore';
export default function ProfileScreen() {
// انتخاب دقیق فیلدهای مورد نیاز — فقط وقتی
// همین فیلدها تغییر کنند، کامپوننت رندر مجدد میشود
const user = useAuthStore((state) => state.user);
const logout = useAuthStore((state) => state.logout);
if (!user) {
return <Text>لطفاً وارد شوید</Text>;
}
return (
<View style={{ padding: 20 }}>
<Image
source={{ uri: user.avatarUrl }}
style={{ width: 100, height: 100, borderRadius: 50 }}
/>
<Text style={{ fontSize: 24, marginTop: 10 }}>
{user.name}
</Text>
<Text style={{ color: '#666' }}>{user.email}</Text>
<TouchableOpacity
onPress={logout}
style={{
marginTop: 20,
backgroundColor: '#ff4444',
padding: 12,
borderRadius: 8,
}}
>
<Text style={{ color: '#fff', textAlign: 'center' }}>
خروج از حساب
</Text>
</TouchableOpacity>
</View>
);
}
نکته مهم: انتخابگر (Selector) برای بهینهسازی رندر
یکی از مهمترین ویژگیهای Zustand، سیستم انتخابگر (Selector) هست. وقتی از Store استفاده میکنید، به جای دریافت کل State، فقط فیلدهای مورد نیازتون رو انتخاب کنید. این باعث میشه کامپوننت فقط زمانی رندر مجدد بشه که همون فیلدهای خاص تغییر کرده باشن:
// ❌ اشتباه: هر تغییری در Store باعث رندر مجدد میشود
const state = useAuthStore();
// ✅ درست: فقط وقتی user تغییر کند رندر مجدد میشود
const user = useAuthStore((state) => state.user);
// ✅ برای چند فیلد از shallow استفاده کنید
import { useShallow } from 'zustand/react/shallow';
const { user, isAuthenticated } = useAuthStore(
useShallow((state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}))
);
ذخیرهسازی دائمی با Persist Middleware
یکی از قابلیتهای واقعاً خوب Zustand، میانافزار persist هست که بهتون اجازه میده State رو به صورت دائمی ذخیره کنید. تو React Native میتونید از AsyncStorage یا MMKV استفاده کنید:
// stores/useSettingsStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface SettingsState {
theme: 'light' | 'dark';
language: string;
notificationsEnabled: boolean;
fontSize: number;
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (language: string) => void;
toggleNotifications: () => void;
setFontSize: (size: number) => void;
}
const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
language: 'fa',
notificationsEnabled: true,
fontSize: 16,
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
toggleNotifications: () =>
set((state) => ({
notificationsEnabled: !state.notificationsEnabled,
})),
setFontSize: (size) => set({ fontSize: size }),
}),
{
name: 'app-settings',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
export default useSettingsStore;
با این تنظیم، وقتی کاربر تم تاریک رو انتخاب میکنه و اپ رو میبنده، دفعه بعد که باز بشه تنظیمات قبلی خودکار بارگذاری میشن. خیلی راحت، نه؟
ترکیب چند Middleware
میتونید چند تا میانافزار رو با هم ترکیب کنید. مثلاً ذخیرهسازی دائمی همراه با ابزارهای دیباگ:
import { create } from 'zustand';
import { persist, createJSONStorage, devtools } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
const useCartStore = create(
devtools(
persist(
(set, get) => ({
items: [],
totalPrice: 0,
addItem: (product) =>
set(
(state) => {
const existingItem = state.items.find(
(item) => item.id === product.id
);
if (existingItem) {
return {
items: state.items.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
),
totalPrice: state.totalPrice + product.price,
};
}
return {
items: [...state.items, { ...product, quantity: 1 }],
totalPrice: state.totalPrice + product.price,
};
},
false,
'cart/addItem'
),
removeItem: (productId) =>
set(
(state) => {
const item = state.items.find((i) => i.id === productId);
return {
items: state.items.filter((i) => i.id !== productId),
totalPrice: state.totalPrice - (item?.price ?? 0) * (item?.quantity ?? 0),
};
},
false,
'cart/removeItem'
),
clearCart: () =>
set({ items: [], totalPrice: 0 }, false, 'cart/clearCart'),
}),
{
name: 'shopping-cart',
storage: createJSONStorage(() => AsyncStorage),
}
),
{ name: 'CartStore' }
)
);
Jotai: مدیریت State اتمی و انعطافپذیر
Jotai (به ژاپنی یعنی «وضعیت») رویکرد متفاوتی نسبت به Zustand داره. به جای ایجاد یه Store مرکزی، از مفهوم «اتم» (Atom) استفاده میکنه — یعنی قطعات کوچک و مستقل از State که میتونن با هم ترکیب بشن.
حجم باندل Jotai فقط حدود ۴ کیلوبایته و کنترل خیلی دقیقی روی رندرهای مجدد فراهم میکنه.
نصب و مفاهیم پایه
npm install jotai
# یا
yarn add jotai
اتمهای پایه (Primitive Atoms)
اتم پایه سادهترین واحد State تو Jotai هست:
// atoms/appAtoms.ts
import { atom } from 'jotai';
// اتمهای پایه
export const themeAtom = atom<'light' | 'dark'>('light');
export const languageAtom = atom<string>('fa');
export const fontSizeAtom = atom<number>(16);
export const isMenuOpenAtom = atom<boolean>(false);
export const searchQueryAtom = atom<string>('');
اتمهای مشتق (Derived Atoms)
قدرت واقعی Jotai تو اتمهای مشتقه — اتمهایی که مقدارشون از ترکیب اتمهای دیگه محاسبه میشه. اینجاست که واقعاً شروع میکنید به لذت بردن:
// atoms/cartAtoms.ts
import { atom } from 'jotai';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
// اتم پایه: لیست آیتمهای سبد خرید
export const cartItemsAtom = atom<CartItem[]>([]);
// اتم مشتق فقط-خواندنی: تعداد کل آیتمها
export const cartItemCountAtom = atom((get) => {
const items = get(cartItemsAtom);
return items.reduce((total, item) => total + item.quantity, 0);
});
// اتم مشتق فقط-خواندنی: مجموع قیمت
export const cartTotalAtom = atom((get) => {
const items = get(cartItemsAtom);
return items.reduce(
(total, item) => total + item.price * item.quantity,
0
);
});
// اتم مشتق فقط-خواندنی: آیا سبد خالی است؟
export const isCartEmptyAtom = atom((get) => {
return get(cartItemsAtom).length === 0;
});
// اتم خواندنی-نوشتنی: افزودن آیتم به سبد
export const addToCartAtom = atom(
null, // مقدار خواندنی ندارد
(get, set, product: Omit<CartItem, 'quantity'>) => {
const currentItems = get(cartItemsAtom);
const existingItem = currentItems.find(
(item) => item.id === product.id
);
if (existingItem) {
set(
cartItemsAtom,
currentItems.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
);
} else {
set(cartItemsAtom, [
...currentItems,
{ ...product, quantity: 1 },
]);
}
}
);
استفاده در کامپوننتها
// components/CartBadge.tsx
import React from 'react';
import { View, Text } from 'react-native';
import { useAtomValue } from 'jotai';
import { cartItemCountAtom } from '../atoms/cartAtoms';
export default function CartBadge() {
// فقط به تعداد آیتمها گوش میدهد
// اگر قیمت یا نام آیتم تغییر کند، رندر نمیشود!
const itemCount = useAtomValue(cartItemCountAtom);
if (itemCount === 0) return null;
return (
<View style={{
backgroundColor: 'red',
borderRadius: 12,
paddingHorizontal: 6,
paddingVertical: 2,
}}>
<Text style={{ color: '#fff', fontSize: 12 }}>
{itemCount}
</Text>
</View>
);
}
// components/CartTotal.tsx
import React from 'react';
import { View, Text } from 'react-native';
import { useAtomValue } from 'jotai';
import { cartTotalAtom } from '../atoms/cartAtoms';
export default function CartTotal() {
const total = useAtomValue(cartTotalAtom);
return (
<View style={{ padding: 16 }}>
<Text style={{ fontSize: 20, fontWeight: 'bold' }}>
مجموع: {total.toLocaleString('fa-IR')} تومان
</Text>
</View>
);
}
اتمهای async برای عملیات ناهمزمان
Jotai از اتمهای ناهمزمان هم پشتیبانی میکنه. این قابلیت برای واکشی داده خیلی کاربردیه:
// atoms/userAtoms.ts
import { atom } from 'jotai';
export const userIdAtom = atom<string | null>(null);
// اتم async: پروفایل کاربر
export const userProfileAtom = atom(async (get) => {
const userId = get(userIdAtom);
if (!userId) return null;
const response = await fetch(
`https://api.example.com/users/${userId}`
);
return response.json();
});
// استفاده با Suspense
// <Suspense fallback={<Loading />}>
// <UserProfile />
// </Suspense>
ذخیرهسازی دائمی در React Native
// atoms/persistedAtoms.ts
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import AsyncStorage from '@react-native-async-storage/async-storage';
const storage = createJSONStorage<any>(() => AsyncStorage);
export const themeAtom = atomWithStorage(
'app-theme',
'light',
storage
);
export const onboardingCompletedAtom = atomWithStorage(
'onboarding-completed',
false,
storage
);
Zustand یا Jotai: کدوم رو انتخاب کنیم؟
هر دو کتابخانه عالین، ولی برای سناریوهای متفاوتی بهینه شدن. بذارید ساده بگم:
- Zustand رو انتخاب کنید اگه: اپلیکیشنتون Storeهای مشخص و جداگانه داره (مثلاً Store احراز هویت، Store تنظیمات، Store سبد خرید). Zustand برای الگوی «یه Store برای هر دامنه» عالیه و APIش شبیه به Redux سادهشدهست.
- Jotai رو انتخاب کنید اگه: Stateهای زیادی دارید که به شکلهای مختلف با هم ترکیب میشن. وقتی نیاز به مشتقگیری پیچیده و کنترل دقیق روی رندرها دارید، مدل اتمی Jotai قدرتمندتره. اگه با Recoil هم آشنایید، Jotai رابط مشابهی داره.
تو تجربه شخصیم، بیشتر پروژهها رو میشه با Zustand مدیریت کرد. ولی اگه واقعاً State پیچیدهای با وابستگیهای زیاد دارید، Jotai انتخاب بهتریه.
TanStack Query: مدیریت هوشمند داده سمت سرور
خب، بریم سراغ ستاره اصلی نمایش برای دادههای سمت سرور. TanStack Query (که قبلاً React Query بود) یه کتابخانه تخصصی برای مدیریت State سمت سرور هست. تمام پیچیدگیهای واکشی داده، کشینگ، بهروزرسانی پسزمینه و همگامسازی رو خودکار مدیریت میکنه.
استفاده ازش تو React Native میتونه حجم کد مربوط به مدیریت داده رو تا ۸۰ درصد کاهش بده. جدی میگم — این عدد اغراق نیست.
نصب و راهاندازی
npm install @tanstack/react-query
# یا
yarn add @tanstack/react-query
تنظیم اولیه Provider
// App.tsx
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // دادهها ۵ دقیقه تازه هستند
gcTime: 30 * 60 * 1000, // کش ۳۰ دقیقه نگهداری میشود
retry: 3, // ۳ بار تلاش مجدد در صورت خطا
refetchOnWindowFocus: false, // در React Native غیرفعال
},
},
});
export default function App() {
return (
<QueryClientProvider client={queryClient}>
{/* بقیه اپلیکیشن */}
</QueryClientProvider>
);
}
واکشی داده با useQuery
هوک useQuery ستون فقرات TanStack Query هست. یه کلید یکتا و یه تابع واکشی بهش بدید و بقیه کارها — کشینگ، بارگذاری مجدد، مدیریت خطا — همه خودکار انجام میشه:
// hooks/useProducts.ts
import { useQuery } from '@tanstack/react-query';
interface Product {
id: string;
name: string;
price: number;
imageUrl: string;
category: string;
}
async function fetchProducts(category?: string): Promise<Product[]> {
const url = category
? `https://api.example.com/products?category=${category}`
: 'https://api.example.com/products';
const response = await fetch(url);
if (!response.ok) {
throw new Error('خطا در دریافت محصولات');
}
return response.json();
}
export function useProducts(category?: string) {
return useQuery({
queryKey: ['products', { category }],
queryFn: () => fetchProducts(category),
staleTime: 10 * 60 * 1000, // ۱۰ دقیقه
});
}
// استفاده در کامپوننت
// components/ProductList.tsx
import React from 'react';
import {
FlatList,
View,
Text,
Image,
ActivityIndicator,
} from 'react-native';
import { useProducts } from '../hooks/useProducts';
export default function ProductList({ category }) {
const {
data: products,
isLoading,
isError,
error,
refetch,
isRefetching,
} = useProducts(category);
if (isLoading) {
return <ActivityIndicator size="large" />;
}
if (isError) {
return (
<View style={{ padding: 20, alignItems: 'center' }}>
<Text style={{ color: 'red' }}>{error.message}</Text>
<TouchableOpacity onPress={refetch}>
<Text>تلاش مجدد</Text>
</TouchableOpacity>
</View>
);
}
return (
<FlatList
data={products}
keyExtractor={(item) => item.id}
refreshing={isRefetching}
onRefresh={refetch}
renderItem={({ item }) => (
<View style={{ flexDirection: 'row', padding: 12 }}>
<Image
source={{ uri: item.imageUrl }}
style={{ width: 80, height: 80, borderRadius: 8 }}
/>
<View style={{ marginStart: 12, flex: 1 }}>
<Text style={{ fontSize: 16, fontWeight: 'bold' }}>
{item.name}
</Text>
<Text style={{ color: '#666' }}>
{item.price.toLocaleString('fa-IR')} تومان
</Text>
</View>
</View>
)}
/>
);
}
تغییر داده با useMutation
هوک useMutation برای عملیاتهایی مثل ایجاد، بهروزرسانی و حذف داده استفاده میشه. ترکیبش با بهروزرسانی خوشبینانه (Optimistic Update) یه تجربه کاربری فوقالعاده سریع میسازه:
// hooks/useToggleFavorite.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
async function toggleFavoriteAPI(productId: string) {
const response = await fetch(
`https://api.example.com/favorites/${productId}`,
{ method: 'POST' }
);
return response.json();
}
export function useToggleFavorite() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: toggleFavoriteAPI,
// قبل از ارسال به سرور: بهروزرسانی خوشبینانه
onMutate: async (productId) => {
// لغو واکشیهای در حال انجام
await queryClient.cancelQueries({
queryKey: ['products'],
});
// ذخیره State قبلی برای بازگردانی
const previousProducts = queryClient.getQueryData([
'products',
]);
// بهروزرسانی فوری UI
queryClient.setQueryData(['products'], (old: any[]) =>
old?.map((product) =>
product.id === productId
? { ...product, isFavorite: !product.isFavorite }
: product
)
);
return { previousProducts };
},
// در صورت خطا: بازگردانی به State قبلی
onError: (err, productId, context) => {
queryClient.setQueryData(
['products'],
context?.previousProducts
);
},
// در هر صورت: همگامسازی مجدد با سرور
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
صفحهبندی نامحدود با useInfiniteQuery
برای لیستهای بلند که نیاز به بارگذاری تدریجی دارن (مثل فید اینستاگرام):
// hooks/useFeed.ts
import { useInfiniteQuery } from '@tanstack/react-query';
async function fetchFeedPage({ pageParam = 1 }) {
const response = await fetch(
`https://api.example.com/feed?page=${pageParam}&limit=20`
);
return response.json();
}
export function useFeed() {
return useInfiniteQuery({
queryKey: ['feed'],
queryFn: fetchFeedPage,
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? allPages.length + 1 : undefined;
},
});
}
// استفاده در کامپوننت
import { useFeed } from '../hooks/useFeed';
export default function FeedScreen() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useFeed();
const allPosts = data?.pages.flatMap((page) => page.items) ?? [];
return (
<FlatList
data={allPosts}
keyExtractor={(item) => item.id}
onEndReached={() => {
if (hasNextPage) fetchNextPage();
}}
onEndReachedThreshold={0.5}
ListFooterComponent={
isFetchingNextPage
? <ActivityIndicator />
: null
}
renderItem={({ item }) => (
<PostCard post={item} />
)}
/>
);
}
پشتیبانی آفلاین
یکی از قابلیتهای خیلی مهم TanStack Query تو اپلیکیشنهای موبایل، پشتیبانی از حالت آفلاین هست. با ترکیب persistQueryClient و AsyncStorage، دادههای کششده حتی بعد از بستن اپ حفظ میشن:
// queryClient.ts
import { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import AsyncStorage from '@react-native-async-storage/async-storage';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // ۲۴ ساعت
staleTime: 1000 * 60 * 5, // ۵ دقیقه
},
},
});
const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'REACT_QUERY_OFFLINE_CACHE',
});
// App.tsx
export default function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister: asyncStoragePersister }}
>
{/* بقیه اپلیکیشن */}
</PersistQueryClientProvider>
);
}
بازواکشی خودکار هنگام فوکوس صفحه
تو React Native میتونید با استفاده از AppState و هوکهای ناوبری، دادهها رو هنگام بازگشت کاربر به صفحه بهروزرسانی کنید:
// hooks/useRefreshOnFocus.ts
import { useEffect } from 'react';
import { AppState } from 'react-native';
import { focusManager } from '@tanstack/react-query';
import { useFocusEffect } from '@react-navigation/native';
// بازواکشی هنگام بازگشت اپ از پسزمینه
export function useAppStateRefresh() {
useEffect(() => {
const subscription = AppState.addEventListener(
'change',
(status) => {
focusManager.setFocused(status === 'active');
}
);
return () => subscription.remove();
}, []);
}
// بازواکشی هنگام فوکوس صفحه در ناوبری
export function useRefreshOnFocus(refetch: () => void) {
useFocusEffect(
useCallback(() => {
refetch();
}, [refetch])
);
}
ترکیب ابزارها: معماری مدرن مدیریت State
خب، حالا بیاید ببینیم چطور این سه تا ابزار رو کنار هم استفاده کنیم. تو یه اپلیکیشن فروشگاهی React Native، معماری مدیریت State میتونه به این شکل باشه:
// ==========================================
// ساختار پروژه
// ==========================================
// src/
// ├── stores/ ← Zustand Stores
// │ ├── useAuthStore.ts
// │ └── useSettingsStore.ts
// ├── atoms/ ← Jotai Atoms
// │ ├── uiAtoms.ts
// │ └── cartAtoms.ts
// ├── hooks/ ← TanStack Query Hooks
// │ ├── useProducts.ts
// │ ├── useOrders.ts
// │ └── useProfile.ts
// └── providers/
// └── AppProviders.tsx
// providers/AppProviders.tsx
import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { Provider as JotaiProvider } from 'jotai';
import { queryClient } from '../queryClient';
export default function AppProviders({
children,
}: {
children: React.ReactNode;
}) {
return (
<QueryClientProvider client={queryClient}>
<JotaiProvider>
{children}
</JotaiProvider>
</QueryClientProvider>
);
}
// توجه: Zustand نیازی به Provider ندارد!
یه نکته جالب: Zustand اصلاً نیازی به Provider نداره. این یعنی میتونید از هر جایی تو اپ بدون هیچ wrapper اضافهای بهش دسترسی داشته باشید.
نمونه عملی: صفحه محصول
تو این مثال، هر سه ابزار کنار هم کار میکنن. دقت کنید هر کدوم چه نقشی داره:
// screens/ProductDetailScreen.tsx
import React from 'react';
import {
View,
Text,
Image,
ScrollView,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import { useQuery } from '@tanstack/react-query';
import { useSetAtom, useAtomValue } from 'jotai';
import useAuthStore from '../stores/useAuthStore';
import { addToCartAtom, cartItemCountAtom } from '../atoms/cartAtoms';
export default function ProductDetailScreen({ route }) {
const { productId } = route.params;
// TanStack Query: واکشی جزئیات محصول از سرور
const {
data: product,
isLoading,
} = useQuery({
queryKey: ['product', productId],
queryFn: () =>
fetch(`https://api.example.com/products/${productId}`)
.then((res) => res.json()),
});
// Zustand: بررسی وضعیت احراز هویت
const isAuthenticated = useAuthStore(
(state) => state.isAuthenticated
);
// Jotai: عملیات سبد خرید
const addToCart = useSetAtom(addToCartAtom);
const cartCount = useAtomValue(cartItemCountAtom);
if (isLoading) {
return <ActivityIndicator size="large" />;
}
return (
<ScrollView style={{ flex: 1 }}>
<Image
source={{ uri: product.imageUrl }}
style={{ width: '100%', height: 300 }}
/>
<View style={{ padding: 16 }}>
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>
{product.name}
</Text>
<Text style={{ fontSize: 20, color: '#2196F3', marginTop: 8 }}>
{product.price.toLocaleString('fa-IR')} تومان
</Text>
<Text style={{ marginTop: 12, lineHeight: 24 }}>
{product.description}
</Text>
<TouchableOpacity
onPress={() => {
if (!isAuthenticated) {
// هدایت به صفحه ورود
return;
}
addToCart({
id: product.id,
name: product.name,
price: product.price,
});
}}
style={{
marginTop: 20,
backgroundColor: '#4CAF50',
padding: 16,
borderRadius: 12,
alignItems: 'center',
}}
>
<Text style={{ color: '#fff', fontSize: 18 }}>
افزودن به سبد ({cartCount})
</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
}
مقایسه کامل: Zustand در مقابل Jotai در مقابل Redux
بیاید این سه کتابخانه رو از چند جنبه مهم کنار هم بذاریم تا انتخاب راحتتر بشه.
حجم باندل و عملکرد
- Zustand: حدود ۱ کیلوبایت — سبکترین گزینه. عملکرد عالی با سیستم انتخابگر
- Jotai: حدود ۴ کیلوبایت — بسیار سبک. کنترل دقیق رندر با اتمها
- Redux Toolkit: حدود ۱۲ کیلوبایت — سنگینترین گزینه. البته ابزارهای دیباگ قدرتمندی داره
منحنی یادگیری
- Zustand: خیلی ملایم — اگه با React هوکها آشنایید، ۱۰ دقیقهای یاد میگیرید
- Jotai: ملایم — مفهوم اتم سادهست ولی الگوهای پیشرفته نیاز به تمرین دارن
- Redux Toolkit: متوسط — مفاهیم slice، thunk و middleware زمان میبرن
مناسب برای
- Zustand: پروژههای کوچک تا بزرگ، تیمهایی که سادگی رو ترجیح میدن
- Jotai: اپلیکیشنهایی با State پیچیده و وابستگیهای زیاد بین Stateها
- Redux Toolkit: پروژههای خیلی بزرگ با تیمهای متعدد که نیاز به ساختار سختگیرانه دارن
بهترین شیوهها و نکات عملی
۱. قانون طلایی تفکیک
این رو حتماً یادتون باشه: هرگز پاسخهای API رو تو Zustand یا Jotai ذخیره نکنید. داده سروری؟ TanStack Query. State کلاینتی (UI، تنظیمات، فرمها)؟ Zustand یا Jotai. همین.
// ❌ اشتباه: ذخیره داده سرور در Zustand
const useProductStore = create((set) => ({
products: [],
isLoading: false,
fetchProducts: async () => {
set({ isLoading: true });
const res = await fetch('/api/products');
const products = await res.json();
set({ products, isLoading: false });
},
}));
// ✅ درست: TanStack Query برای داده سرور
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: () =>
fetch('/api/products').then((res) => res.json()),
});
}
// ✅ درست: Zustand فقط برای State سمت کلاینت
const useUIStore = create((set) => ({
selectedFilter: 'all',
sortOrder: 'newest',
setFilter: (filter) => set({ selectedFilter: filter }),
setSortOrder: (order) => set({ sortOrder: order }),
}));
۲. الگوی کلید واکشی (Query Key) ساختاریافته
برای پروژههای بزرگ، کلیدهای واکشی رو با الگوی کارخانهای مدیریت کنید. این کار تو بلندمدت کلی وقت صرفهجویی میکنه:
// queryKeys.ts
export const productKeys = {
all: ['products'] as const,
lists: () => [...productKeys.all, 'list'] as const,
list: (filters: object) =>
[...productKeys.lists(), filters] as const,
details: () => [...productKeys.all, 'detail'] as const,
detail: (id: string) =>
[...productKeys.details(), id] as const,
};
// استفاده
useQuery({
queryKey: productKeys.detail(productId),
queryFn: () => fetchProduct(productId),
});
// باطلسازی تمام لیستها
queryClient.invalidateQueries({
queryKey: productKeys.lists(),
});
۳. تستپذیری
هر سه کتابخانه به راحتی تستپذیرن. Zustand حتی امکان دسترسی به State بدون نیاز به کامپوننت React رو فراهم میکنه — و این برای unit test نوشتن خیلی عالیه:
// تست Zustand بدون React
import useAuthStore from '../stores/useAuthStore';
describe('AuthStore', () => {
beforeEach(() => {
useAuthStore.setState({
user: null,
token: null,
isAuthenticated: false,
});
});
it('should login user', () => {
const mockUser = {
id: '1',
name: 'علی',
email: '[email protected]',
avatarUrl: '',
};
useAuthStore.getState().login(mockUser, 'token123');
expect(useAuthStore.getState().isAuthenticated).toBe(true);
expect(useAuthStore.getState().user?.name).toBe('علی');
});
});
جمعبندی
مدیریت State تو React Native در سال ۲۰۲۶ دیگه یه مسئله «یه ابزار برای همه کارها» نیست. رویکرد مدرن، ترکیب هوشمندانه ابزارهای تخصصیه:
- Zustand برای مدیریت State سمت کلاینت با API ساده و عملکرد عالی
- Jotai برای Stateهای اتمی با وابستگیهای پیچیده و کنترل دقیق رندر
- TanStack Query برای مدیریت هوشمند داده سمت سرور با کشینگ خودکار
با تفکیک State سمت کلاینت از State سمت سرور و استفاده از ابزار مناسب برای هر کدوم، کد تمیزتر، عملکرد بهتر و تجربه توسعه لذتبخشتری خواهید داشت.
پیشنهادم اینه که با TanStack Query برای تمام واکشیهای داده شروع کنید، بعد Zustand یا Jotai رو برای State باقیمونده اضافه کنید. به زودی میبینید که چطور این ترکیب، پیچیدگی مدیریت State رو به شکل چشمگیری کم میکنه. امتحانش کنید — پشیمون نمیشید.