در ری اکت، ارسال داده از کامپوننت پدر به کامپوننت فرزند به طور معمول از طریق props انجام می شود. اما در شرایطی استفاده از prop ها پیچیده و باعث کاهش خوانایی و نگهداری کد می شود. در این مقاله به بررسی مفهوم prop drilling می پردازیم و می بینیم چطور می توانیم با استفاده از Context API این مسئله را حل کنیم. برای دسترسی به دوره های آموزشی تخصصی تر می توانید به بخش آموزش React مراجعه کنید.
Prop drilling چیست؟
همانطور که می دانید برای ارسال داده از کامپوننت پدر به کامپوننت فرزند از props استفاده می شود. به عبارت بهتر اگر کامپوننت فرزند بخواهد به یک قطعه اطلاعاتی که در کامپوننت پدر موجود است، دسترسی داشته باشد، باید از طریق props این داده از پدر به فرزند ارسال شود. مشکل زمانی به وجود می آید که لازم باشد داده در چندین سطح به کامپوننت های زیر مجموعه ارسال شود. یعنی از کامپوننت پدر به فرزند و از کامپوننت فرزند به نوه و ...
فرآیند انتقال داده در چندین سطح از درخت کامپوننت را در اصلاح prop drilling می نامیم. در یک درخت کامپوننت، چندین سطح از کامپوننت ها وجود دارد اما اکثر کامپوننت های میانی نیازی به این داده ها ندارند بلکه تنها وظیفه انتقال داده به کامپوننت فرزند خود را دارند. در کد زیر این ساختار را می توانید مشاهده کنید:
const Grandpa = (props) => {
return (<Dad story = {props.story} />);
}
const Dad = (props) => {
return (<Son story = {props.story} />);
}
const Son = (props) => {
return (<Grandson story = {props.story} />);
}
const Grandson = (props) => {
return (<p>Here's the story that was passed down to the Grandson component:{props.story}</p>);
}
export default Grandpa;
چگونه از Context API برای حل این مشکل استفاده کنیم؟
Prop drilling لزوما یک مشکل نیست. در اکثر موارد دقیقا همان چیزی است که باید انجام دهید. اما اگر داده یا تابعی دارید که باید به شکل سراسری (سراسری نسبت به درخت کامپوننت) تعریف شود، Context API کار را ساده تر می کند.
نحوه کارکرد Context API به صورت زیر است:
- در ابتدا لازم است یک context object تعریف شود. این آبجکت شامل یک کامپوننت Provider و یک کامپوننت Consumer است.
- وظیفه Provider تولید داده برای کامپوننت های زیر مجموعه است. به عبارت بهتر Provider داده مورد نیاز را برای استفاده کامپوننت های زیر مجموعه فراهم می کند. این داده می تواند یک آبجکت باشد یا یک تابع.
- هر کدام از کامپوننت های زیر مجموعه، در سلسله مراتب درخت کامپوننت، می توانند به Provider اصطلاحا subscribe کند. یعنی درخواست دریافت داده را به Provider ارسال کند. در این حالت این کامپوننت آماده است تا مقدار ارسالی از کامپوننت Provider را دریافت کند.
- در نهایت کامپوننت هایی که در Provider قرار گرفته باشند با هر تغییر داده مجددا رندر (re-render) می شوند.
پیاده سازی Context API
تا اینجا در مورد مسئله Prop drilling صحبت کردیم و دیدیم برای مدیریت بهتر این مسئله بهتر است از Context API استفاده کنیم. در این بخش از مقاله، نحوه پیاده سازی Context API را بررسی می کنیم.
برای ساخت یک Context، از React.createContext استفاده می کنیم:
const MyContext = React.createContext(defaultValue);
متد createContext یک context object را بر می گرداند. Default value مقدار پیش فرض است. البته در اکثر موارد در این مرحله مقدار اولیه ای وجود ندارد در نتیجه بهتر است مقدار آن undefined یا نهایتا یک مقدار پیش فرض باشد.
در کد زیر یک context object برای اطلاعات مربوط به ترجیحات کاربر تعریف شده است:
const PrefsContext = React.createContext({lang:'English',timezone:'PacificTime'});
پس از تعریف context object لازم است Provider را تعریف کنیم. همانطور که گفته شد Provider کامپوننتی است که تغییرات داده را به کامپوننت های زیر مجموعه خود منتشر می کند. Provider خصوصیتی به نام value دارد. داده ها در حقیقت از طریق این خصوصیت به کامپونت های داخلی ارسال می شوند.
<MyContext.Provider value={/*some value here*/}>
برای استفاده از Provider لازم است بیرونی ترین کامپوننت در درخت کامپوننت به عنوان child در Provider component قرار گیرد. اما روش بهتر این است Provider را به گونه ای تعریف کنیم که امکان استفاده مجدد از آن برای درخت های دیگر هم وجود داشته باشد. برای این کار می توانیم به صورت زیر عمل کنیم:
import React, {useState} from 'react';
import {PrefsContext} from './contexts/UserPrefs';
const UserPrefsProvider = ({ children }) => {
const [lang, setLang] = useState("English");
const [timezone, setTimezone] = useState("UTC");
return (
<PrefsContext.Provider value={{ lang, timezone }}>
{children}
</PrefsContext.Provider>
);
};
function App(){
return (
<UserPrefsProvider>
<Header />
<Main />
<Footer />
</UserPrefsProvider>
)
}
export default App;
همانطور که می بینید با استفاده از روش فوق، UserPresProvider component را می توانیم هر جایی که لازم باشد استفاده کنیم. البته در این مثال آن را در app component تعریف کرده ایم که کل اپلیکیشن را در بر می گیرد.
در این نقطه Context object و Provider component تعریف شده اند. اکنون کامپوننت های داخلی می توانند مقدار تولید شده را استفاده کنند. در ری اکت ما از اصطلاح مصرف کردن یا consume کردن برای استفاده از مقادیر Provider استفاده می کنیم. نکته مهم این است که با هر تغییر خصوصیت value، تمامی کامپوننت های مصرف کننده که در Provider قرار گرفته اند، مجدد رندر می شوند.
برای مصرف کردن یک Context می توانیم از دو طریق اقدام کنیم: یا از طریق کامپوننت Context.Consumer و یا از طریق useContext hook.
برای استفاده از useContext hook ابتدا باید context object تعریف شده را import کنیم و به useContext ارسال کنیم. با این کار مقدار ذخیره شده در value به عنوان خروجی بر می گردد. کد زیر نحوه consume کردن را نشان می دهد:
import React from 'react';
import {PrefsContext} from './contexts/UserPrefs';
class TimeDisplay extends React.Component {
static contextType = PrefsContext;
render() {
return (
<>
Your language preference is {this.context.lang}.<br />
Your timezone is {this.context.timezone}.
</>
)
}
}
export default TimeDisplay;
در چه سناریوهایی می توان از Context API استفاده کرد؟
به طور کلی Context API برای مدیریت داده های global یا سراسری مورد استفاده قرار می گیرد. به طور دقیق تر می توان گفت، چنانچه لازم است چندین کامپوننت در سطوح مختلف یک درخت، به داده مشترکی دسترسی داشته باشند، در این حالت Context API یکی از بهترین انتخاب هاست.
در سناریوهایی زیر می توان از Context API استفاده نمود:
- تغییر تم اپلیکیشن ( به عنوان مثال برای تغییر بین تم تاریک و تم روشن)
- اطلاعات مربوط به ترجیحات کاربر یا user preferences
- چند زبانه بودن اپلیکیشن
برای بررسی سناریوهای بیشتر می توانید به بخش آموزش ری اکت مراجعه کنید.
چه زمانی نباید از Context API استفاده کنیم؟
زمانی که یک کامپوننت از Context استفاده می کند، در حقیقت به یک global state وابسته می شود. این اتفاق باعث کاهش قابلیت استفاده مجدد کامپوننت می شود. چانچه می خواهید کامپوننتی تعریف کنید که در بخش های مختلف اپلیکیشن استفاده شود توصیه می شود از Context API استفاده نکنید. روش های دیگری برای حل مسئله prop drilling وجود دارد که می توان از این روش ها استفاده کرد. یکی از این روش ها composition pattern است که در این مقاله به طور کامل آن را مورد بررسی قرار داده ایم.
همچنین چنانچه نرخ تغییرات Context value بالا باشد، بهتر است از Context API استفاده نکنید چرا که با هر تغییر State، تمامی کامپوننت های زیر مجموعه یا به طور دقیق تر کامپوننت های مصرف کننده مجدد رندر می شوند. به همین دلیل استفاده از Context API در شرایطی که نرخ تغییرات بالاست توصیه نمی شود. استفاده از تغییر تم یکی از بهترین استفاده های Context API است چرا که نرخ تغییر تم پایین است در نتیجه با مشکل عدم کارایی مواجه نخواهیم شد.
خلاصه
Context API یکی از بهترین روش ها برای برخورد با مسئله prop drilling است. قدرت این روش با معرفی useContext بی اندازه بیشتر شده است.البته دقت کنید در استفاده از این روش زیاده روی نشود، چرا که باعث کاهش قابلیت استفاده مجدد کامپوننت ها می شود. به طور کلی Context API برای مدیریت داده های کوچک اما به شکل مشترک بین کامپوننت های یک درخت کامپوننت مورد استفاده قرار می گیرد.