الگوهای پیشرفته React Context API شامل کامپوننتهای ترکیبی، کانتکستهای پویا و تکنیکهای بهینهسازی عملکرد برای مدیریت وضعیتهای پیچیده را کاوش کنید.
الگوهای پیشرفته React Context API برای مدیریت وضعیت
React Context API مکانیزم قدرتمندی برای به اشتراکگذاری وضعیت در سراسر برنامه شما بدون نیاز به prop drilling فراهم میکند. در حالی که استفاده اولیه از آن ساده است، بهرهبرداری از تمام پتانسیل آن نیازمند درک الگوهای پیشرفتهای است که میتوانند سناریوهای مدیریت وضعیت پیچیده را مدیریت کنند. این مقاله چندین مورد از این الگوها را بررسی میکند و با ارائه مثالهای عملی و بینشهای کاربردی، به ارتقاء توسعه React شما کمک میکند.
درک محدودیتهای Context API پایه
قبل از پرداختن به الگوهای پیشرفته، شناخت محدودیتهای Context API پایه بسیار مهم است. اگرچه این API برای وضعیتهای ساده و قابل دسترس به صورت سراسری مناسب است، اما برای برنامههای پیچیده با وضعیتهایی که به طور مکرر تغییر میکنند، میتواند ناکارآمد و سنگین شود. هر کامپوننتی که از یک کانتکست استفاده میکند، با هر تغییر در مقدار کانتکست، دوباره رندر میشود، حتی اگر آن کامپوننت به بخشی از وضعیت که تغییر کرده است، وابسته نباشد. این موضوع میتواند منجر به گلوگاههای عملکردی شود.
الگوی ۱: کامپوننتهای ترکیبی (Compound Components) با Context
الگوی کامپوننت ترکیبی، Context API را با ایجاد مجموعهای از کامپوننتهای مرتبط که به طور ضمنی وضعیت و منطق را از طریق یک کانتکست به اشتراک میگذارند، تقویت میکند. این الگو قابلیت استفاده مجدد را ترویج داده و API را برای مصرفکنندگان سادهتر میکند. این کار امکان کپسولهسازی منطق پیچیده با یک پیادهسازی ساده را فراهم میآورد.
مثال: یک کامپوننت Tab
بیایید این موضوع را با یک کامپوننت Tab نشان دهیم. به جای پاس دادن props از طریق چندین لایه، کامپوننتهای Tab
به طور ضمنی از طریق یک کانتکست مشترک با یکدیگر ارتباط برقرار میکنند.
// TabContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface TabContextType {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabContext = createContext(undefined);
interface TabProviderProps {
children: ReactNode;
defaultTab: string;
}
export const TabProvider: React.FC = ({ children, defaultTab }) => {
const [activeTab, setActiveTab] = useState(defaultTab);
const value: TabContextType = {
activeTab,
setActiveTab,
};
return {children} ;
};
export const useTabContext = () => {
const context = useContext(TabContext);
if (!context) {
throw new Error('useTabContext must be used within a TabProvider');
}
return context;
};
// TabList.js
import React, { ReactNode } from 'react';
interface TabListProps {
children: ReactNode;
}
export const TabList: React.FC = ({ children }) => {
return {children};
};
// Tab.js
import React, { ReactNode } from 'react';
import { useTabContext } from './TabContext';
interface TabProps {
label: string;
children: ReactNode;
}
export const Tab: React.FC = ({ label, children }) => {
const { activeTab, setActiveTab } = useTabContext();
const isActive = activeTab === label;
const handleClick = () => {
setActiveTab(label);
};
return (
);
};
// TabPanel.js
import React, { ReactNode } from 'react';
import { useTabContext } from './TabContext';
interface TabPanelProps {
label: string;
children: ReactNode;
}
export const TabPanel: React.FC = ({ label, children }) => {
const { activeTab } = useTabContext();
const isActive = activeTab === label;
return (
{isActive && children}
);
};
// نحوه استفاده
import { TabProvider, TabList, Tab, TabPanel } from './components/Tabs';
function App() {
return (
Tab 1
Tab 2
Tab 3
Content for Tab 1
Content for Tab 2
Content for Tab 3
);
}
export default App;
مزایا:
- API سادهتر برای مصرفکنندگان: کاربران فقط باید نگران
Tab
،TabList
وTabPanel
باشند. - اشتراکگذاری ضمنی وضعیت: کامپوننتها به طور خودکار به وضعیت مشترک دسترسی پیدا کرده و آن را بهروزرسانی میکنند.
- قابلیت استفاده مجدد بهبود یافته: کامپوننت
Tab
را میتوان به راحتی در کانتکستهای مختلف دوباره استفاده کرد.
الگوی ۲: کانتکستهای پویا (Dynamic Contexts)
در برخی سناریوها، ممکن است بر اساس موقعیت کامپوننت در درخت کامپوننتها یا سایر عوامل پویا، به مقادیر کانتکست متفاوتی نیاز داشته باشید. کانتکستهای پویا به شما این امکان را میدهند که مقادیر کانتکستی را ایجاد و فراهم کنید که بر اساس شرایط خاصی متغیر باشند.
مثال: تمبندی (Theming) با کانتکستهای پویا
یک سیستم تمبندی را در نظر بگیرید که در آن میخواهید تمهای مختلفی را بر اساس ترجیحات کاربر یا بخشی از برنامه که در آن قرار دارند، ارائه دهید. میتوانیم یک مثال ساده با تم روشن و تیره بسازیم.
// ThemeContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface Theme {
background: string;
color: string;
}
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const defaultTheme: Theme = {
background: 'white',
color: 'black'
};
const darkTheme: Theme = {
background: 'black',
color: 'white'
};
const ThemeContext = createContext({
theme: defaultTheme,
toggleTheme: () => {}
});
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC = ({ children }) => {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const theme = isDarkTheme ? darkTheme : defaultTheme;
const toggleTheme = () => {
setIsDarkTheme(!isDarkTheme);
};
const value: ThemeContextType = {
theme,
toggleTheme,
};
return {children} ;
};
export const useTheme = () => {
return useContext(ThemeContext);
};
// نحوه استفاده
import { useTheme, ThemeProvider } from './ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useTheme();
return (
This is a themed component.
);
}
function App() {
return (
);
}
export default App;
در این مثال، ThemeProvider
به صورت پویا تم را بر اساس وضعیت isDarkTheme
تعیین میکند. کامپوننتهایی که از هوک useTheme
استفاده میکنند، با تغییر تم به طور خودکار دوباره رندر میشوند.
الگوی ۳: Context با useReducer برای وضعیت پیچیده
برای مدیریت منطق وضعیت پیچیده، ترکیب Context API با useReducer
یک رویکرد عالی است. useReducer
روشی ساختاریافته برای بهروزرسانی وضعیت بر اساس اکشنها فراهم میکند و Context API به شما اجازه میدهد این وضعیت و تابع dispatch را در سراسر برنامه خود به اشتراک بگذارید.
مثال: یک لیست کارهای ساده (Todo List)
// TodoContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
}
type TodoAction =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: number }
| { type: 'DELETE_TODO'; id: number };
interface TodoContextType {
state: TodoState;
dispatch: React.Dispatch;
}
const initialState: TodoState = {
todos: [],
};
const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.id),
};
default:
return state;
}
};
const TodoContext = createContext(undefined);
interface TodoProviderProps {
children: ReactNode;
}
export const TodoProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(todoReducer, initialState);
const value: TodoContextType = {
state,
dispatch,
};
return {children} ;
};
export const useTodo = () => {
const context = useContext(TodoContext);
if (!context) {
throw new Error('useTodo must be used within a TodoProvider');
}
return context;
};
// نحوه استفاده
import { useTodo, TodoProvider } from './TodoContext';
function TodoList() {
const { state, dispatch } = useTodo();
return (
{state.todos.map((todo) => (
-
{todo.text}
))}
);
}
function AddTodo() {
const { dispatch } = useTodo();
const [text, setText] = React.useState('');
const handleSubmit = (e) => {
e.preventDefault();
dispatch({ type: 'ADD_TODO', text });
setText('');
};
return (
);
}
function App() {
return (
);
}
export default App;
این الگو منطق مدیریت وضعیت را در reducer متمرکز میکند و استدلال در مورد آن و تست کردنش را آسانتر میسازد. کامپوننتها میتوانند اکشنها را برای بهروزرسانی وضعیت dispatch کنند بدون اینکه نیاز به مدیریت مستقیم وضعیت داشته باشند.
الگوی ۴: بهروزرسانیهای بهینه Context با `useMemo` و `useCallback`
همانطور که قبلاً ذکر شد، یک نکته کلیدی در عملکرد Context API، رندرهای مجدد غیرضروری است. استفاده از useMemo
و useCallback
میتواند با اطمینان از اینکه فقط بخشهای ضروری مقدار کانتکست بهروز میشوند و مراجع توابع پایدار میمانند، از این رندرهای مجدد جلوگیری کند.
مثال: بهینهسازی یک Theme Context
// OptimizedThemeContext.js
import React, { createContext, useContext, useState, useMemo, useCallback, ReactNode } from 'react';
interface Theme {
background: string;
color: string;
}
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const defaultTheme: Theme = {
background: 'white',
color: 'black'
};
const darkTheme: Theme = {
background: 'black',
color: 'white'
};
const ThemeContext = createContext({
theme: defaultTheme,
toggleTheme: () => {}
});
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC = ({ children }) => {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const theme = isDarkTheme ? darkTheme : defaultTheme;
const toggleTheme = useCallback(() => {
setIsDarkTheme(!isDarkTheme);
}, [isDarkTheme]);
const value: ThemeContextType = useMemo(() => ({
theme,
toggleTheme,
}), [theme, toggleTheme]);
return {children} ;
};
export const useTheme = () => {
return useContext(ThemeContext);
};
توضیح:
useCallback
تابعtoggleTheme
را memoize میکند. این کار تضمین میکند که مرجع تابع فقط زمانی کهisDarkTheme
تغییر میکند، تغییر کند و از رندرهای مجدد غیرضروری کامپوننتهایی که فقط به تابعtoggleTheme
وابستهاند، جلوگیری میکند.useMemo
مقدار کانتکست را memoize میکند. این کار تضمین میکند که مقدار کانتکست فقط زمانی کهtheme
یا تابعtoggleTheme
تغییر میکند، تغییر کند و از رندرهای مجدد غیرضروری بیشتر جلوگیری میکند.
بدون useCallback
، تابع toggleTheme
در هر رندر ThemeProvider
دوباره ایجاد میشود، که باعث تغییر value
و راهاندازی رندرهای مجدد در هر کامپوننت مصرفکننده میشود، حتی اگر خود تم تغییر نکرده باشد. useMemo
تضمین میکند که یک value
جدید فقط زمانی ایجاد میشود که وابستگیهای آن (theme
یا toggleTheme
) تغییر کنند.
الگوی ۵: انتخابگرهای کانتکست (Context Selectors)
انتخابگرهای کانتکست به کامپوننتها اجازه میدهند تا فقط در بخشهای خاصی از مقدار کانتکست مشترک شوند. این کار از رندرهای مجدد غیرضروری هنگامی که سایر بخشهای کانتکست تغییر میکنند، جلوگیری میکند. برای دستیابی به این هدف میتوان از کتابخانههایی مانند `use-context-selector` یا پیادهسازیهای سفارشی استفاده کرد.
مثال استفاده از یک انتخابگر کانتکست سفارشی
// useCustomContextSelector.js
import { useContext, useState, useRef, useEffect } from 'react';
function useCustomContextSelector(
context: React.Context,
selector: (value: T) => S
): S {
const value = useContext(context);
const [selected, setSelected] = useState(() => selector(value));
const latestSelector = useRef(selector);
latestSelector.current = selector;
useEffect(() => {
let didUnmount = false;
let lastSelected = selected;
const subscription = () => {
if (didUnmount) {
return;
}
const nextSelected = latestSelector.current(value);
if (!Object.is(lastSelected, nextSelected)) {
lastSelected = nextSelected;
setSelected(nextSelected);
}
};
// شما معمولاً در اینجا در تغییرات کانتکست مشترک میشوید. از آنجا که این یک مثال ساده شده است،
// ما فقط برای مقداردهی اولیه، فوراً اشتراک را فراخوانی میکنیم.
subscription();
return () => {
didUnmount = true;
// در صورت لزوم، در اینجا اشتراک را از تغییرات کانتکست لغو کنید.
};
}, [value]); // هر زمان که مقدار کانتکست تغییر میکند، افکت را دوباره اجرا کنید
return selected;
}
export default useCustomContextSelector;
// ThemeContext.js (سادهشده برای اختصار)
import React, { createContext, useState, ReactNode } from 'react';
interface Theme {
background: string;
color: string;
}
interface ThemeContextType {
theme: Theme;
setTheme: (newTheme: Theme) => void;
}
const ThemeContext = createContext(undefined);
interface ThemeProviderProps {
children: ReactNode;
initialTheme: Theme;
}
export const ThemeProvider: React.FC = ({ children, initialTheme }) => {
const [theme, setTheme] = useState(initialTheme);
const value: ThemeContextType = {
theme,
setTheme
};
return {children} ;
};
export const useThemeContext = () => {
const context = React.useContext(ThemeContext);
if (!context) {
throw new Error("useThemeContext must be used within a ThemeProvider");
}
return context;
};
export default ThemeContext;
// نحوه استفاده
import useCustomContextSelector from './useCustomContextSelector';
import ThemeContext, { ThemeProvider, useThemeContext } from './ThemeContext';
function BackgroundComponent() {
const background = useCustomContextSelector(ThemeContext, (context) => context.theme.background);
return Background;
}
function ColorComponent() {
const color = useCustomContextSelector(ThemeContext, (context) => context.theme.color);
return Color;
}
function App() {
const { theme, setTheme } = useThemeContext();
const toggleTheme = () => {
setTheme({ background: theme.background === 'white' ? 'black' : 'white', color: theme.color === 'black' ? 'white' : 'black' });
};
return (
);
}
export default App;
در این مثال، BackgroundComponent
فقط زمانی که خاصیت background
تم تغییر میکند دوباره رندر میشود و ColorComponent
فقط زمانی که خاصیت color
تغییر میکند دوباره رندر میشود. این کار از رندرهای مجدد غیرضروری هنگامی که کل مقدار کانتکست تغییر میکند، جلوگیری میکند.
الگوی ۶: جدا کردن اکشنها از وضعیت
برای برنامههای بزرگتر، جدا کردن مقدار کانتکست به دو کانتکست مجزا را در نظر بگیرید: یکی برای وضعیت و دیگری برای اکشنها (توابع dispatch). این کار میتواند سازماندهی کد و قابلیت تست را بهبود بخشد.
مثال: لیست کارها با کانتکستهای وضعیت و اکشن جداگانه
// TodoStateContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
}
const initialState: TodoState = {
todos: [],
};
const TodoStateContext = createContext(initialState);
interface TodoStateProviderProps {
children: ReactNode;
}
export const TodoStateProvider: React.FC = ({ children }) => {
const [state] = useReducer(todoReducer, initialState);
return {children} ;
};
export const useTodoState = () => {
return useContext(TodoStateContext);
};
// TodoActionContext.js
import React, { createContext, useContext, Dispatch, ReactNode } from 'react';
type TodoAction =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: number }
| { type: 'DELETE_TODO'; id: number };
const TodoActionContext = createContext | undefined>(undefined);
interface TodoActionProviderProps {
children: ReactNode;
}
export const TodoActionProvider: React.FC = ({children}) => {
const [, dispatch] = useReducer(todoReducer, initialState);
return {children} ;
};
export const useTodoDispatch = () => {
const dispatch = useContext(TodoActionContext);
if (!dispatch) {
throw new Error('useTodoDispatch must be used within a TodoActionProvider');
}
return dispatch;
};
// todoReducer.js
export const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.id),
};
default:
return state;
}
};
// نحوه استفاده
import { useTodoState, TodoStateProvider } from './TodoStateContext';
import { useTodoDispatch, TodoActionProvider } from './TodoActionContext';
function TodoList() {
const state = useTodoState();
return (
{state.todos.map((todo) => (
-
{todo.text}
))}
);
}
function TodoActions({ todo }) {
const dispatch = useTodoDispatch();
return (
<>
>
);
}
function AddTodo() {
const dispatch = useTodoDispatch();
const [text, setText] = React.useState('');
const handleSubmit = (e) => {
e.preventDefault();
dispatch({ type: 'ADD_TODO', text });
setText('');
};
return (
);
}
function App() {
return (
);
}
export default App;
این جداسازی به کامپوننتها اجازه میدهد تا فقط در کانتکستی که به آن نیاز دارند مشترک شوند و رندرهای مجدد غیرضروری را کاهش میدهد. همچنین تست واحد reducer و هر کامپوننت به صورت جداگانه را آسانتر میکند. همچنین، ترتیب قرارگیری providerها مهم است. ActionProvider
باید StateProvider
را در بر بگیرد.
بهترین شیوهها و ملاحظات
- کانتکست نباید جایگزین تمام کتابخانههای مدیریت وضعیت شود: برای برنامههای بسیار بزرگ و پیچیده، کتابخانههای اختصاصی مدیریت وضعیت مانند Redux یا Zustand ممکن است همچنان انتخاب بهتری باشند.
- از استفاده بیش از حد از کانتکست خودداری کنید: هر بخشی از وضعیت نیازی به قرار گرفتن در کانتکست ندارد. از کانتکست با احتیاط برای وضعیتهای واقعاً سراسری یا به طور گسترده به اشتراک گذاشته شده استفاده کنید.
- تست عملکرد: همیشه تأثیر استفاده از کانتکست بر عملکرد را بسنجید، به خصوص هنگام کار با وضعیتهایی که به طور مکرر بهروز میشوند.
- Code Splitting (تقسیم کد): هنگام استفاده از Context API، تقسیم کد برنامه به بخشهای کوچکتر را در نظر بگیرید. این امر به ویژه زمانی مهم است که یک تغییر کوچک در وضعیت باعث رندر مجدد بخش بزرگی از برنامه میشود.
نتیجهگیری
React Context API یک ابزار همهکاره برای مدیریت وضعیت است. با درک و به کارگیری این الگوهای پیشرفته، میتوانید به طور موثر وضعیت پیچیده را مدیریت کنید، عملکرد را بهینه سازید و برنامههای React قابل نگهداری و مقیاسپذیرتری بسازید. به یاد داشته باشید که الگوی مناسب را برای نیازهای خاص خود انتخاب کنید و پیامدهای عملکردی استفاده از کانتکست را به دقت در نظر بگیرید.
همانطور که React تکامل مییابد، بهترین شیوههای مربوط به Context API نیز تغییر خواهند کرد. آگاه ماندن از تکنیکها و کتابخانههای جدید تضمین میکند که شما برای مقابله با چالشهای مدیریت وضعیت در توسعه وب مدرن مجهز هستید. بررسی الگوهای نوظهور مانند استفاده از کانتکست با سیگنالها برای واکنشپذیری دقیقتر را در نظر بگیرید.