فرانت اند

مدیریت state در ریکت بدون پیچیدگی

بیشتر پروژه‌های ریکتی که کند و درهم‌ریخته شده‌اند، کمبود ابزار نداشتند؛ همه‌چیز را در یک store سراسری ریخته بودند. مدیریت state در ریکت وقتی ساده می‌شود که اول نوع state را تشخیص بدهید و بعد ابزار را انتخاب کنید.

  • نویسنده تیم تتا
  • ۸ دقیقه مطالعه
  • به‌روزرسانی:
مدیریت state در ریکت بدون پیچیدگی

پنج نوع state که نباید قاطی شوند

وقتی می‌گویند «مدیریت state»، در واقع چند مسئله‌ی متفاوت زیر یک اسم جمع شده‌اند. جدول زیر کمک می‌کند هر کدام را به جای درستش بفرستید:

نوع state و ابزار پیشنهادی
نوعمثالابزار مناسب
محلیباز یا بسته‌بودن منو، متن یک inputuseState
مشترکانتخاب تب که دو کامپوننت می‌خوانندبالا بردن state به والد
URLفیلتر، مرتب‌سازی، شماره‌ی صفحهsearchParams / router
سرورفهرست محصولات، پروفایل کاربرTanStack Query یا SWR
سراسری کلاینتیسبد خرید، پیش‌نویس فرم چندمرحله‌ایZustand، Redux Toolkit

بخش بزرگی از «مشکل state» با همین دسته‌بندی حل می‌شود: داده‌ی سرور را از store سراسری بیرون بکشید و ببینید چقدر از آن باقی می‌ماند. مستندات رسمی react.dev هم همین رویکرد را توصیه می‌کند.

تصویر انتزاعی از چند ظرف رنگی با اندازه‌های مختلف که با خطوط نورانی به هم وصل شده‌اند
اول نوع state را تشخیص دهید؛ ابزار درست معمولاً خودش مشخص می‌شود.

از state محلی شروع کنید

useState برای ۸۰٪ نیازها کافی است و بهترین ویژگی‌اش این است که وقتی کامپوننت حذف شد، state هم پاک می‌شود. فقط وقتی دو کامپوننت هم‌سطح به یک مقدار نیاز داشتند، آن را به نزدیک‌ترین والد مشترک ببرید.

state مشتق‌شده را ذخیره نکنید

یکی از رایج‌ترین باگ‌ها، نگه‌داشتن یک مقدار محاسبه‌شده در state جداگانه و فراموش‌کردن همگام‌سازی آن است. به‌جای آن در رندر محاسبه کنید:

ts
// Bad: two sources of truth
const [items, setItems] = useState<Item[]>([]);
const [total, setTotal] = useState(0);

// Good: total is derived
const [items, setItems] = useState<Item[]>([]);
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);

اگر محاسبه واقعاً سنگین است، useMemo می‌تواند کمک کند، اما اول اندازه بگیرید؛ بیشتر محاسبه‌ها آن‌قدر ارزان‌اند که memo فقط کد را شلوغ می‌کند.

Context و useReducer؛ کی و چطور

Context مکانیزم انتقال مقدار است، نه ابزار مدیریت state. هر بار مقدار Provider عوض شود، همه‌ی مصرف‌کننده‌ها دوباره رندر می‌شوند؛ برای تم، زبان یا کاربر واردشده مشکلی نیست، ولی برای مقداری که چند بار در ثانیه تغییر می‌کند دردسر می‌سازد.

وقتی منطق به‌روزرسانی چند شاخه دارد (مثلاً مراحل یک فرم)، useReducer ترتیب را شفاف می‌کند. هر تغییر یک action با نام روشن است و تست‌کردنش آسان:

ts
type State = { step: number; email: string };
type Action = { type: 'next' } | { type: 'back' } | { type: 'setEmail'; value: string };

function reducer(s: State, a: Action): State {
  switch (a.type) {
    case 'next': return { ...s, step: Math.min(s.step + 1, 3) };
    case 'back': return { ...s, step: Math.max(s.step - 1, 1) };
    case 'setEmail': return { ...s, email: a.value };
  }
}

const [state, dispatch] = useReducer(reducer, { step: 1, email: '' });

داده‌ی سرور را جدا مدیریت کنید

داده‌ی API یک نسخه‌ی کپی‌شده از چیزی است که مالکش سرور است. این کپی کهنه می‌شود، باید دوباره گرفته شود، کش شود و خطا و حالت بارگذاری دارد. نوشتن همه‌ی این‌ها با useEffect و useState هم پرکد است و هم پر از شرایط مسابقه (race condition). کتابخانه‌هایی مثل TanStack Query این مسئله را حل کرده‌اند:

ts
import { useQuery } from '@tanstack/react-query';

function Products({ page }: { page: number }) {
  const { data, isPending, error } = useQuery({
    queryKey: ['products', page],
    queryFn: () => fetch(`/api/products?page=${page}`).then(r => r.json())
  });
  if (isPending) return <p>Loading…</p>;
  if (error) return <p>Something went wrong.</p>;
  return <ul>{data.items.map((p: any) => <li key={p.id}>{p.name}</li>)}</ul>;
}

اگر از فریم‌ورکی مثل نکست استفاده می‌کنید، بخش بزرگی از داده‌گیری اولیه را می‌توان در سرور انجام داد؛ جزئیاتش در مقاله‌ی ویژگی‌های Next.js 13 آمده است.

کی store سراسری لازم است؟

store سراسری برای state کلاینتی‌ای است که بین بخش‌های دور از هم اپلیکیشن به اشتراک گذاشته می‌شود و از سرور نمی‌آید؛ مثل سبد خرید، پنل‌های باز، یا پیش‌نویس ذخیره‌نشده. Zustand با کمترین کد این کار را می‌کند و کامپوننت‌ها فقط به تکه‌ای که می‌خوانند مشترک می‌شوند:

ts
import { create } from 'zustand';

type CartState = { ids: number[]; add: (id: number) => void; clear: () => void };

export const useCart = create<CartState>(set => ({
  ids: [],
  add: id => set(s => ({ ids: [...s.ids, id] })),
  clear: () => set({ ids: [] })
}));

// Only re-renders when the count changes
const count = useCart(s => s.ids.length);

Redux Toolkit هم انتخاب معتبری است، به‌ویژه در تیم‌های بزرگ که قاعده‌ی یکسان و ابزار دیباگ قوی می‌خواهند. انتخاب بین این‌ها بیشتر مسئله‌ی تیم است تا فنی.

چک‌لیست و اشتباه‌های رایج

  • قبل از اضافه‌کردن هر store بپرسید: آیا این مقدار از URL، سرور یا state دیگر قابل‌محاسبه نیست؟
  • فیلترها و صفحه‌بندی را در URL نگه دارید تا لینک‌پذیر باشند و با رفرش از بین نروند.
  • اشیای بزرگ را در Context نگذارید؛ چند Context کوچک بهتر از یک Context بزرگ است.
  • state را مستقیم تغییر ندهید (push روی آرایه‌ی state)؛ نسخه‌ی جدید بسازید.
  • برای ساختار کلی کامپوننت‌ها، اصول کامپوننت‌نویسی و برای تایپ‌دادن state، راهنمای تایپ‌اسکریپت را ببینید.

ساختار درست state معمولاً کد کمتر و رندر کمتر می‌دهد. اگر می‌خواهید معماری فرانت‌اند پروژه‌تان را مرور کنیم، از همین‌جا با ما تماس بگیرید.

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

آیا برای هر پروژه‌ی ریکت به Redux نیاز دارم؟

خیر. بیشتر پروژه‌ها با useState، URL و یک کتابخانه‌ی server state کار می‌کنند. Redux یا Zustand وقتی لازم می‌شوند که state کلاینتی مشترک بین بخش‌های دور از هم زیاد شود.

Context API جای کتابخانه‌ی state را می‌گیرد؟

برای مقدارهای کم‌تغییر مثل تم یا زبان بله، اما برای state پرتغییر نه. هر تغییر مقدار، همه‌ی مصرف‌کننده‌ها را دوباره رندر می‌کند و همین مقیاس‌پذیری را محدود می‌کند.

تفاوت state سرور و state کلاینت چیست؟

state سرور کپی داده‌ای است که مالکش سرور است و کهنه می‌شود، مثل فهرست محصولات. state کلاینت فقط در مرورگر وجود دارد، مثل باز بودن یک منو. هر کدام ابزار مناسب خودش را دارد.

Zustand بهتر است یا Redux Toolkit؟

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

دیدگاه‌ها

    هنوز دیدگاهی ثبت نشده است. اولین نفر باشید.

    دیدگاه‌ها پس از بررسی منتشر می‌شوند.