مقالات › React

مدیریت State در React؛ از useState تا ابزارهای حرفه‌ای

راهنمای عملی انتخاب ابزار درست برای State در React: State محلی، بالا بردن State، Context، Reducer، Zustand، Redux Toolkit و State سرور با TanStack Query.

هر اپلیکیشن React در نهایت یک ماشین وضعیت است. سؤال سخت این نیست که State را چطور ذخیره کنیم؛ این است که کجا. اگر خیلی بالا قرارش دهید، هر کلید که فشرده می‌شود نیمی از برنامه را دوباره رندر می‌کند؛ اگر خیلی پایین باشد، کامپوننت‌ها نمی‌توانند آن را به اشتراک بگذارند؛ و اگر زود سراغ یک کتابخانه‌ی سنگین بروید، قابلیت‌های ساده زیر کدهای تکراری غرق می‌شوند. این راهنما یک چارچوب تصمیم‌گیری از useState تا کتابخانه‌های اختصاصی در اختیارتان می‌گذارد.

چهار نوع State

بیشتر سردرگمی‌ها وقتی از بین می‌رود که نوع State را درست دسته‌بندی کنید:

نوع مثال بهترین جا
State محلی رابط کاربری مقدار ورودی، باز بودن مودال، تب فعال useState داخل همان کامپوننت
State مشترک کلاینت تم، زبان، سبد خرید، کاربر واردشده State بالابرده‌شده، Context یا یک Store
State سرور مقالات، محصولات، پروفایل کاربر از API TanStack Query / SWR / Loaderهای فریم‌ورک
State آدرس فیلترها، شماره صفحه، تبی که قابل اشتراک باشد آدرس (Search Params)

هر نوع نیازهای متفاوتی دارد. State سرور به کش، دریافت مجدد و ابطال نیاز دارد؛ State محلی به هیچ‌کدام.

۱. با useState محلی شروع کنید

function NewsletterForm() {
  const [email, setEmail] = useState("");
  const [status, setStatus] = useState("idle"); // idle | sending | done | error

  // ...
}

رایج‌ترین اشتباه، سراسری کردن State «محض احتیاط» است. Stateی که فقط یک کامپوننت از آن استفاده می‌کند، جایش در همان کامپوننت است. فهمیدن، تست کردن و حذف کردنش ساده‌تر است.

۲. وقتی کامپوننت‌های هم‌سطح نیاز دارند، State را بالا ببرید

وقتی دو کامپوننت به یک داده نیاز دارند، State را به نزدیک‌ترین والد مشترکشان منتقل کنید و به پایین پاس بدهید:

function ProductPage() {
  const [quantity, setQuantity] = useState(1);
  return (
    <>
      <QuantityPicker value={quantity} onChange={setQuantity} />
      <PriceSummary quantity={quantity} />
    </>
  );
}

بالا بردن State ساده و صریح است. فقط وقتی دردسر می‌شود که Propها از لایه‌های زیادی عبور کنند که از آن‌ها استفاده نمی‌کنند (Prop Drilling).

۳. useReducer برای تغییرات پیچیده

وقتی به‌روزرسانی‌ها از قوانینی پیروی می‌کنند — فرم چندمرحله‌ای یا سبد خرید با تخفیف — Reducer تغییرات را در یک جا نگه می‌دارد:

function cartReducer(state, action) {
  switch (action.type) {
    case "add": {
      const existing = state.items.find((i) => i.id === action.item.id);
      const items = existing
        ? state.items.map((i) => (i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i))
        : [...state.items, { ...action.item, qty: 1 }];
      return { ...state, items };
    }
    case "remove":
      return { ...state, items: state.items.filter((i) => i.id !== action.id) };
    case "clear":
      return { ...state, items: [] };
    default:
      return state;
  }
}

۴. Context برای داده‌های سراسری کم‌تغییر

Context برای داده‌ای که کامپوننت‌های زیادی می‌خوانند، Prop Drilling را حذف می‌کند:

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, login: setUser, logout: () => setUser(null) }), [user]);
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export const useAuth = () => useContext(AuthContext);

نکته‌ی مهم: با تغییر مقدار Context همه‌ی مصرف‌کننده‌ها دوباره رندر می‌شوند. Context برای تم، زبان، کاربر واردشده و Feature Flagها — چیزهایی که کم تغییر می‌کنند — عالی است. برای مقادیر پرتغییر (فیلد متنی، تایمر، موقعیت ماوس) رندرهای غیرضروری ایجاد می‌کند.

راه‌های کاهش این مشکل:

  • Contextها را بر اساس دغدغه جدا کنید (ThemeContext، AuthContext) به‌جای یک AppContext غول‌پیکر.
  • شیء مقدار را با useMemo Memo کنید.
  • State و Dispatch را در دو Context جدا قرار دهید تا کامپوننت‌هایی که فقط Dispatch می‌کنند دوباره رندر نشوند.

۵. State سرور متفاوت است؛ از کتابخانه‌ی State سرور استفاده کنید

داده‌ای که از API می‌آید واقعاً «مال شما» نیست: روی سرور است، بدون اطلاع شما تغییر می‌کند و به کش نیاز دارد. مدیریت آن با useState + useEffect یعنی پیاده‌سازی دوباره‌ی وضعیت بارگذاری، خطا، کش، حذف درخواست تکراری، تلاش مجدد و Race Condition در هر کامپوننت.

TanStack Query این مشکل را حل می‌کند:

function ArticleList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ["articles"],
    queryFn: () => fetch("/api/articles").then((r) => r.json()),
    staleTime: 60_000,
  });

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return data.map((a) => <ArticleCard key={a.id} article={a} />);
}

function useCreateArticle() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (article) => api.create(article),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ["articles"] }),
  });
}

وقتی State سرور به یک کش کوئری منتقل شود، بیشتر اپلیکیشن‌ها متوجه می‌شوند State سراسری کلاینتشان به‌طرز شگفت‌آوری کم است.

۶. Store برای State پیچیده‌ی کلاینت: Zustand

برای State مشترک کلاینت که زیاد تغییر می‌کند — سبد خرید، ویرایشگر، چیدمان داشبورد — یک کتابخانه‌ی Store کوچک از مشکل رندر Context جلوگیری می‌کند. Zustand مینیمال است و اجازه می‌دهد کامپوننت‌ها فقط در بخشی از State مشترک شوند:

import { create } from "zustand";

export const useCart = create((set) => ({
  items: [],
  add: (item) => set((s) => ({ items: [...s.items, item] })),
  remove: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
  clear: () => set({ items: [] }),
}));

function CartBadge() {
  const count = useCart((s) => s.items.length);   // فقط با تغییر تعداد دوباره رندر می‌شود
  return <span>{count}</span>;
}

۷. Redux Toolkit برای تیم‌های بزرگ

Redux Toolkit همچنان گزینه‌ی محکمی است وقتی قراردادهای سخت‌گیرانه، DevTools قدرتمند (دیباگ با سفر در زمان) و یک جریان داده‌ی واحد و قابل پیش‌بینی در یک تیم بزرگ می‌خواهید. Redux مدرن با createSlice خیلی کم‌حجم‌تر از Redux کلاسیک است و RTK Query هم State سرور را پوشش می‌دهد.

۸. آدرس را فراموش نکنید

فیلترها، عبارت جستجو، صفحه‌بندی و تب انتخاب‌شده اغلب جایشان در آدرس است تا کاربر بتواند نشانه‌گذاری و به اشتراک بگذارد:

const [params, setParams] = useSearchParams();
const tag = params.get("tag") ?? "all";

چک‌لیست تصمیم‌گیری

  1. فقط یک کامپوننت استفاده می‌کند؟ ← useState.
  2. چند کامپوننت نزدیک به هم استفاده می‌کنند؟ ← State را بالا ببرید.
  3. تغییرات پیچیده دارد؟ ← useReducer.
  4. از API می‌آید؟ ← TanStack Query (یا لایه‌ی داده‌ی فریم‌ورک).
  5. باید قابل اشتراک/نشانه‌گذاری باشد؟ ← آدرس.
  6. سراسری و کم‌تغییر است؟ ← Context.
  7. سراسری، پرتغییر و پیچیده است؟ ← Zustand یا Redux Toolkit.

جمع‌بندی

با ساده‌ترین ابزار شروع کنید و فقط وقتی درد واقعی را حس کردید سراغ ابزار سنگین‌تر بروید. State را به‌طور پیش‌فرض محلی نگه دارید، در صورت نیاز بالا ببرید، از Context برای داده‌های سراسری کم‌تغییر، از کتابخانه‌ی کوئری برای داده‌ی سرور، از آدرس برای State قابل اشتراک و از Storeهایی مثل Zustand یا Redux Toolkit فقط برای State واقعاً پیچیده‌ی کلاینت استفاده کنید. دسته‌بندی درست State، ۹۰ درصد معماری خوب React است.

پرسش‌های متداول

آیا Context جایگزین Redux است؟

Context سازوکار تزریق وابستگی است، نه مدیر State با Selector و DevTools. برای داده‌های کم‌تغییر کافی است؛ برای State پیچیده و پرتغییر، Store عملکرد بهتری دارد.

آیا State فرم باید سراسری باشد؟

تقریباً هرگز. آن را محلی نگه دارید (یا از کتابخانه‌ای مثل React Hook Form استفاده کنید) و به سرور ارسال کنید.

Zustand یا Redux Toolkit؟

Zustand برای اپلیکیشن‌های کوچک تا متوسط که کمترین کد تکراری را می‌خواهند؛ Redux Toolkit برای تیم‌های بزرگ که ساختار سخت‌گیرانه و ابزارها برایشان ارزش دارد.