اكتشف أنماطًا متقدمة لواجهة برمجة تطبيقات سياق React، بما في ذلك المكونات المركبة، والسياقات الديناميكية، وتقنيات تحسين الأداء لإدارة الحالات المعقدة.
أنماط متقدمة لواجهة برمجة تطبيقات سياق React (Context API) لإدارة الحالة
توفر واجهة برمجة تطبيقات سياق React (Context API) آلية قوية لمشاركة الحالة عبر تطبيقك دون الحاجة إلى تمرير الخصائص (prop drilling). في حين أن الاستخدام الأساسي بسيط ومباشر، فإن استغلال إمكاناتها الكاملة يتطلب فهم الأنماط المتقدمة التي يمكنها التعامل مع سيناريوهات إدارة الحالة المعقدة. يستكشف هذا المقال العديد من هذه الأنماط، ويقدم أمثلة عملية ورؤى قابلة للتنفيذ للارتقاء بتطوير React الخاص بك.
فهم قيود واجهة برمجة تطبيقات السياق الأساسية
قبل الغوص في الأنماط المتقدمة، من الضروري الإقرار بقيود واجهة برمجة تطبيقات السياق الأساسية. على الرغم من أنها مناسبة للحالات البسيطة التي يمكن الوصول إليها عالميًا، إلا أنها يمكن أن تصبح غير عملية وغير فعالة للتطبيقات المعقدة ذات الحالة المتغيرة بشكل متكرر. كل مكون يستهلك سياقًا يعيد العرض (re-render) كلما تغيرت قيمة السياق، حتى لو كان المكون لا يعتمد على الجزء المحدد من الحالة الذي تم تحديثه. يمكن أن يؤدي هذا إلى اختناقات في الأداء.
النمط الأول: المكونات المركبة (Compound Components) مع السياق
يعزز نمط المكونات المركبة واجهة برمجة تطبيقات السياق عن طريق إنشاء مجموعة من المكونات المترابطة التي تشارك الحالة والمنطق ضمنيًا من خلال سياق. يعزز هذا النمط إمكانية إعادة الاستخدام ويبسط واجهة برمجة التطبيقات للمستهلكين. وهذا يسمح بتغليف المنطق المعقد بتنفيذ بسيط.
مثال: مكون علامات التبويب (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}
);
};
// Usage
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;
الفوائد:
- واجهة برمجة تطبيقات مبسطة للمستهلكين: يحتاج المستخدمون فقط إلى الاهتمام بـ
Tab
وTabList
وTabPanel
. - مشاركة الحالة ضمنيًا: تصل المكونات تلقائيًا إلى الحالة المشتركة وتقوم بتحديثها.
- إعادة استخدام محسنة: يمكن إعادة استخدام مكون
Tab
بسهولة في سياقات مختلفة.
النمط الثاني: السياقات الديناميكية
في بعض السيناريوهات، قد تحتاج إلى قيم سياق مختلفة بناءً على موضع المكون في شجرة المكونات أو عوامل ديناميكية أخرى. تسمح لك السياقات الديناميكية بإنشاء وتوفير قيم سياق تختلف بناءً على شروط محددة.
مثال: تطبيق السمات (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);
};
// Usage
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
ستتم إعادة عرضها تلقائيًا عند تغيير السمة.
النمط الثالث: السياق مع `useReducer` للحالات المعقدة
لإدارة منطق الحالة المعقد، يعد الجمع بين واجهة برمجة تطبيقات السياق و useReducer
نهجًا ممتازًا. يوفر useReducer
طريقة منظمة لتحديث الحالة بناءً على الإجراءات (actions)، وتسمح لك واجهة برمجة تطبيقات السياق بمشاركة هذه الحالة ودالة الإرسال (dispatch) عبر تطبيقك.
مثال: قائمة مهام بسيطة
// 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;
};
// Usage
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، مما يسهل فهمه واختباره. يمكن للمكونات إرسال الإجراءات لتحديث الحالة دون الحاجة إلى إدارة الحالة مباشرة.
النمط الرابع: تحسين تحديثات السياق باستخدام `useMemo` و `useCallback`
كما ذكرنا سابقًا، أحد الاعتبارات الرئيسية للأداء مع واجهة برمجة تطبيقات السياق هو عمليات إعادة العرض غير الضرورية. يمكن أن يمنع استخدام useMemo
و useCallback
عمليات إعادة العرض هذه عن طريق التأكد من تحديث الأجزاء الضرورية فقط من قيمة السياق، وأن مراجع الدوال تظل ثابتة.
مثال: تحسين سياق السمات
// 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
بحفظ (memoizes) دالةtoggleTheme
. هذا يضمن أن مرجع الدالة يتغير فقط عندما تتغيرisDarkTheme
، مما يمنع عمليات إعادة العرض غير الضرورية للمكونات التي تعتمد فقط على دالةtoggleTheme
. - يقوم
useMemo
بحفظ قيمة السياق. هذا يضمن أن قيمة السياق تتغير فقط عندما تتغير إما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);
}
};
// You would typically subscribe to context changes here. Since this is a simplified
// example, we'll just call subscription immediately to initialize.
subscription();
return () => {
didUnmount = true;
// Unsubscribe from context changes here, if applicable.
};
}, [value]); // Re-run effect whenever the context value changes
return selected;
}
export default useCustomContextSelector;
// ThemeContext.js (Simplified for brevity)
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;
// Usage
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
. هذا يتجنب عمليات إعادة العرض غير الضرورية عند تغيير قيمة السياق بأكملها.
النمط السادس: فصل الإجراءات (Actions) عن الحالة (State)
للتطبيقات الأكبر، فكر في فصل قيمة السياق إلى سياقين منفصلين: واحد للحالة والآخر للإجراءات (دوال الإرسال). يمكن أن يؤدي ذلك إلى تحسين تنظيم الكود وقابلية الاختبار.
مثال: قائمة مهام مع سياقات منفصلة للحالة والإجراءات
// 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;
}
};
// Usage
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;
يسمح هذا الفصل للمكونات بالاشتراك فقط في السياق الذي تحتاجه، مما يقلل من عمليات إعادة العرض غير الضرورية. كما أنه يسهل اختبار الوحدة (unit test) للـ reducer وكل مكون على حدة. أيضًا، ترتيب تغليف الموفرين (providers) مهم. يجب أن يغلف ActionProvider
الـ StateProvider
.
أفضل الممارسات والاعتبارات
- لا يجب أن يحل السياق محل جميع مكتبات إدارة الحالة: بالنسبة للتطبيقات الكبيرة جدًا والمعقدة، قد تظل مكتبات إدارة الحالة المخصصة مثل Redux أو Zustand خيارًا أفضل.
- تجنب الإفراط في استخدام السياق: لا تحتاج كل قطعة من الحالة إلى أن تكون في سياق. استخدم السياق بحكمة للحالة العالمية الحقيقية أو المشتركة على نطاق واسع.
- اختبار الأداء: قم دائمًا بقياس تأثير استخدامك للسياق على الأداء، خاصة عند التعامل مع الحالة التي يتم تحديثها بشكل متكرر.
- تقسيم الكود (Code Splitting): عند استخدام واجهة برمجة تطبيقات السياق، فكر في تقسيم تطبيقك إلى أجزاء أصغر. هذا مهم بشكل خاص عندما يتسبب تغيير صغير في الحالة في إعادة عرض جزء كبير من التطبيق.
الخلاصة
إن واجهة برمجة تطبيقات سياق React هي أداة متعددة الاستخدامات لإدارة الحالة. من خلال فهم وتطبيق هذه الأنماط المتقدمة، يمكنك إدارة الحالة المعقدة بفعالية، وتحسين الأداء، وبناء تطبيقات React أكثر قابلية للصيانة والتوسع. تذكر أن تختار النمط المناسب لاحتياجاتك الخاصة وأن تدرس بعناية الآثار المترتبة على الأداء عند استخدامك للسياق.
مع تطور React، ستتطور أيضًا أفضل الممارسات المحيطة بواجهة برمجة تطبيقات السياق. سيضمن لك البقاء على اطلاع بالتقنيات والمكتبات الجديدة أن تكون مجهزًا للتعامل مع تحديات إدارة الحالة في تطوير الويب الحديث. فكر في استكشاف الأنماط الناشئة مثل استخدام السياق مع الإشارات (signals) لتفاعلية أكثر دقة.