فارسی

الگوهای پیشرفته 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 ( ); };
// نحوه استفاده
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;

مزایا:

الگوی ۲: کانتکست‌های پویا (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 (
setText(e.target.value)} />
); } 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 در هر رندر 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 (
setText(e.target.value)} />
); } function App() { return ( ); } export default App;

این جداسازی به کامپوننت‌ها اجازه می‌دهد تا فقط در کانتکستی که به آن نیاز دارند مشترک شوند و رندرهای مجدد غیرضروری را کاهش می‌دهد. همچنین تست واحد reducer و هر کامپوننت به صورت جداگانه را آسان‌تر می‌کند. همچنین، ترتیب قرارگیری providerها مهم است. ActionProvider باید StateProvider را در بر بگیرد.

بهترین شیوه‌ها و ملاحظات

نتیجه‌گیری

React Context API یک ابزار همه‌کاره برای مدیریت وضعیت است. با درک و به کارگیری این الگوهای پیشرفته، می‌توانید به طور موثر وضعیت پیچیده را مدیریت کنید، عملکرد را بهینه سازید و برنامه‌های React قابل نگهداری و مقیاس‌پذیرتری بسازید. به یاد داشته باشید که الگوی مناسب را برای نیازهای خاص خود انتخاب کنید و پیامدهای عملکردی استفاده از کانتکست را به دقت در نظر بگیرید.

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