انتقال پراپها (props) از میان لایههایی از کامپوننتهای بیربط، کار خستهکنندهای است. یک روز در حال عرضه یک ویژگی جدید هستید و روز بعد، فقط برای تغییر نام یک پراپ واحد، مجبورید شش فایل را ویرایش کنید. این دقیقاً همان مفهوم Prop Drilling است: یک والد دادهای دارد، یک فرزندِ دوردست به آن نیاز دارد و هر کامپوننتی که بین آنها قرار گرفته، تبدیل به یک پیک (courier) میشود. اپلیکیشن همچنان اجرا میشود، اما کد پایه (codebase) شکننده میشود. اگر یک لایه میانی را حذف کنید، نیمی از درخت کامپوننتها فرو میریزد. اگر یک تایپ را تغییر دهید، TypeScript در سه دایرکتوری مختلف خطا میدهد. React Context API برای حذف کامل این واسطهها وجود دارد.
Prop Drilling در واقعیت چگونه است
یک پوسته استاندارد اپلیکیشن را تصور کنید. شما یک کامپوننت App دارید که کاربر فعلی را واکشی (fetch) میکند. داخل App یک Layout قرار دارد، داخل Layout یک Sidebar نشسته است، داخل Sidebar یک Navigation قرار گرفته و در نهایت داخل Navigation شما UserAvatar را پیدا میکنید که واقعاً به شیء کاربر نیاز دارد.
کد شما در نهایت به این شکل در میآید:
function App() {
const user = { name: 'Aarav', role: 'admin' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <Navigation user={user} />;
}
function Navigation({ user }) {
return <UserAvatar user={user} />;
}
کامپوننتهای Layout ،Sidebar و Navigation هیچ کاری با آن شیء کاربر انجام نمیدهند، جز اینکه آن را به لایه پایینتر تحویل دهند. آنها پراپهایی را جمعآوری میکنند که مالک آنها نیستند، رابطهای کاربری (interfaces) آنها متورم میشود و تست کردن آنها مستلزم شبیهسازی (mocking) دادههایی است که هرگز با آنها درگیر نمیشوند. جرم اصلی این است که این وضعیت چقدر سریع گسترش مییابد. یک پرچم isLoggedIn ،یک رشته locale یا یک مقدار theme اضافه کنید، و همین چرخه تکراری دوباره شروع میشود.
Context API چگونه بازی را تغییر میدهد
Context API را مانند یک روتر وایفای تصور کنید. به جای کشیدن کابلهای طولانی از هر اتاق برای رسیدن به هر دستگاه، روتر سیگنال را از طریق هوا ارسال میکند. هر دستگاهی که در محدوده باشد میتواند مستقیماً متصل شود. در اصطلاحات React، روتر همان Provider است، سیگنال همان state یا داده شماست و دستگاه، هر کامپوننتِ تودرتویی است که useContext را فراخوانی میکند.
این ساختار دارای سه بخش اصلی است:
React.createContext()کانال داده را میسازد.- Provider بخشی از درخت شما را در بر میگیرد و یک مقدار را مخابره میکند.
- هوک
useContextبه فرزندان اجازه میدهد بدون دست زدن به پراپهای میانی، آن مقدار را دریافت کنند.
شما همچنان یک درخت دارید، اما شاخههای بین ریشه و برگ دیگر نیازی ندارند که بر سر قراردادِ پیک بودن توافق کنند.
ساخت یک Context از صفر
بیایید یک مثال ملموس با تنظیمات تم (theme) بسازیم، چرا که اکثر اپلیکیشنها در مقطعی به حالت روشن یا تاریک نیاز دارند.
ابتدا، شیء context را ایجاد کنید. این همان لوله انتقال است:
import { createContext, useState, useMemo } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeContext;
سپس اپلیکیشن خود را در Provider قرار دهید. معمولاً این کار در نزدیکی ریشه (root) انجام میشود:
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
حالا هر فرزند میتواند مستقیماً از سیگنال استفاده کند. در اینجا یک دکمه تغییر وضعیت (toggle) وجود دارد که در اعماق رابط کاربری دفن شده است:
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}
>
Current theme: {theme}
</button>
);
}
دقت کنید که Layout ،Sidebar و Navigation هرگز پراپ theme را نمیبینند. آنها به طور عادی رندر میشوند و ThemeToggle آنچه را که نیاز دارد مستقیماً از context میگیرد. سیمکشی از بیرون نامرئی است، و هدف دقیقاً همین است.
Context واقعاً کجا کاربرد دارد
Context برای دادههایی که بسیاری از کامپوننتهای دوردست با هم به اشتراک میگذارند اما هیچ والد واحدی مالک اصلی آنها نیست، بهترین عملکرد را دارد. کاندیداهای مناسب عبارتند از:
- تنظیمات تم مانند حالت روشن یا تاریک، رنگهای تاکیدی یا مقیاس فونت.
- وضعیت احراز هویت مانند شیء کاربر فعلی، وضعیت ورود یا انقضای نشست (session).
- زبان و لوکال (locale) برای بینالمللیسازی.
- دادههای سبد خرید که باید در سراسر نشانگر هدر، منوی کشویی سبد خرید کوچک و صفحه پرداخت همگام (sync) بمانند.
در برابر وسوسه ریختن هر تکه از وضعیت محلی (local state) در Context مقاومت کنید. یک ورودی فرم که دو سطح پایینتر قرار دارد، نیازی به پخش سراسری ندارد. Context را برای دغدغههای واقعی و مشترک (cross-cutting concerns) نگه دارید و اجازه دهید بقیه موارد به صورت پراپهای معمولی باقی بمانند.
تلههای عملکرد و نحوه اجتناب از آنها
Context رایگان نیست. وقتی یک مقدار context بهروز میشود، هر کامپوننتی که به آن context متصل است دوباره رندر (re-render) میشود، حتی اگر آن بخش از مقداری که به آن اهمیت میدهد تغییر نکرده باشد. اشتباه کلاسیک این است که در هر بار رندر شدن والد، یک شیء جدید (object literal) را به Provider پاس بدهید.
در مثال تم ما، هر بار که ThemeProvider به دلیل بهروز شدن والدِ خودش دوباره رندر میشود، عبارت { theme, setTheme } یک شیء کاملاً جدید ایجاد میکند. React یک مرجع (reference) جدید میبیند و تمام مصرفکنندگان (consumers) بهروز میشوند. اگر تم شما به ندرت تغییر میکند اما وضعیت اپلیکیشن شما اغلب تغییر میکند، شما هزینه رندرهایی را میپردازید که به آنها نیازی ندارید.
راه حل دو مرحلهای است.
کانتکستهای خود را بر اساس فرکانس بهروزرسانی جدا کنید. یک UserContext که هر بار ورود تغییر میکند، نباید با یک NotificationContext که هر چند ثانیه یک بار بهروز میشود، در یک Provider مشترک باشد. آنها را جدا نگه دارید تا دادههای ایستا (static) با دادههای پرنوسان (volatile) در یک قطارِ رندر مجدد قرار نگیرند.
زمانی که مقدار یک شیء یا آرایه است، آن را در useMemo قرار دهید. به React یک مرجع پایدار بدهید:
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Now the object identity only shifts when theme actually changes. Descendant components that care about the context but are shielded by React.memo further down will skip the work.
Mistakes That Waste Your Time
The two errors that still show up in production code are easy to prevent.
First, forgetting to export the context itself. If you only export the Provider wrapper and keep the context object private, a developer writing a new feature cannot call useContext without refactoring your module. Export the context so consumers can import both the provider and the consumer hook cleanly.
Second, calling useContext outside the corresponding Provider. If ThemeToggle renders in a branch of the tree that is not wrapped in ThemeProvider, the hook returns the default value passed to createContext, or undefined if you passed none. This leads to silent failures like cannot read property of undefined. You can guard against this by assigning a sensible default or throwing a clear error early in the hook call.
Context vs Redux: Keep It Simple
You do not always need Redux. For medium-sized projects, Context paired with useState or useReducer covers the bulk of state sharing. Redux shines when you need time-travel debugging, complex middleware, or global transactions that must roll back in sequence. If your entire state story is a user object, a theme string, and a cart array, a store library adds boilerplate you will never leverage.
That said, Context is not a full state management system on its own. It does not give you a single global snapshot, and it does not batch updates across unrelated contexts. Use it as a replacement for prop drilling, not as an operating system for your entire data layer.
The Real Takeaway
Stop threading props through components that do not care about them. Create focused contexts for the data that actually spans your tree, wrap providers high enough to cover the consumers, and always stabilize the value object when you are passing collections or functions. Context API keeps yourReact code direct: props stay local, global data travels wirelessly, and your component boundaries stay clean.
