من useState إلى Redux وZustand وJotai: متى تستخدم كل أداة لإدارة الحالة في React، وكيف تختار دون أن تضيع في دوامة الخيارات؟ دليل عملي مبني على تجارب حقيقية في شركات مثل Spotify وAirbnb.
في يوم من الأيام، كنت أعمل على لوحة تحكم لإدارة محتوى الفيديو في منصة تشبه نتفليكس. كان لدينا ١٢ مكوناً مختلفاً يتفاعلون مع نفس البيانات: قائمة الفيديوات، الفلتر، اللاعب، والتعليقات. بعد أسبوعين من التطوير، أصبح الكود أشبه بشبكة عنكبوتية من الـ props drilling، وكل تعديل بسيط كان يتسبب في كسر مكونات غير متوقعة. المشكلة لم تكن في React نفسها، بل في أننا اخترنا الأداة الخطأ لإدارة الحالة منذ البداية. هذا المقال هو ما تمنيت لو كان موجوداً أمامي وقتها: دليل واضح لاختيار أداة إدارة الحالة المناسبة، دون تعقيد أو نظريات أكاديمية.
عندما نتحدث عن إدارة الحالة في React، فإننا في الواقع نتحدث عن كيفية تنظيم الذاكرة المؤقتة للتطبيق. كل أداة من الأدوات المتاحة - سواء كانت useState البسيطة أو Redux المعقدة - هي في جوهرها مجرد طريقة لتنظيم الـ heap memory في المتصفح. الفرق الرئيسي بينها ليس في قدرتها على حل المشكلة، بل في كيفية تأثيرها على أداء التطبيق، وقابلية الصيانة، وسهولة التوسع. في هذا المقال، سأشرح لك ليس فقط متى تستخدم كل أداة، بل أيضاً ماذا يحدث خلف الكواليس عندما تستخدمها.
لنبدأ بالأبسط. useState هو الخط الأول للدفاع في أي تطبيق React. عندما تستخدم useState، فإنك في الواقع تطلب من React تخصيص مساحة صغيرة في الذاكرة لتخزين قيمة ما، وتزويدك بوظيفة لتحديثها. لكن ما لا يراه الكثيرون هو أن React لا تقوم بتحديث القيمة على الفور. بدلاً من ذلك، تضيف تحديثاً إلى قائمة الانتظار في الـ Event Loop، ثم تقوم بتشغيل عملية الـ reconciliation في وقت لاحق. هذا يعني أن تحديثات useState ليست متزامنة، ولكنها تبدو كذلك للمطور لأن React تقوم بتجميعها في دفعة واحدة.
في معظم الحالات، useState كافية تماماً. مثلاً، إذا كنت تبني نموذج تسجيل دخول بسيط، أو قائمة مهام صغيرة، أو حتى مكون فلتر بسيط، فلا داعي لتعقيد الأمور. المشكلة تبدأ عندما تحتاج إلى مشاركة الحالة بين مكونات متعددة، أو عندما تصبح منطق التحديث معقداً جداً. هنا يأتي دور useReducer. useReducer ليس مجرد بديل لـ useState، بل هو أداة لإدارة الحالة المعقدة محلياً. بدلاً من كتابة عدة دوال useState، يمكنك كتابة reducer واحد يحتوي على كل منطق التحديث. هذا يجعل الكود أكثر قابلية للتنبؤ، وأسهل في الاختبار.
// مثال على useReducer لإدارة حالة نموذج تسجيل معقدة
import { useReducer } from 'react';
const initialState = {
email: '',
password: '',
isLoading: false,
error: null,
showPassword: false
};
function reducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return { ...state, [action.field]: action.value };
case 'TOGGLE_SHOW_PASSWORD':
return { ...state, showPassword: !state.showPassword };
case 'SUBMIT_START':
return { ...state, isLoading: true, error: null };
case 'SUBMIT_SUCCESS':
return { ...initialState };
case 'SUBMIT_ERROR':
return { ...state, isLoading: false, error: action.error };
default:
return state;
}
}
function LoginForm() {
const [state, dispatch] = useReducer(reducer, initialState);
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'SUBMIT_START' });
try {
await login(state.email, state.password);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (err) {
dispatch({ type: 'SUBMIT_ERROR', error: err.message });
}
};
return (
<form {handleSubmit}>
<input
type="email"
value={state.email}
onChange={(e) => dispatch({ type: 'SET_FIELD', field: 'email', value: e.target.value })}
/>
<input
type={state.showPassword ? "text" : "password"}
value={state.password}
onChange={(e) => dispatch({ type: 'SET_FIELD', field: 'password', value: e.target.value })}
/>
<button type="button" onClick={() => dispatch({ type: 'TOGGLE_SHOW_PASSWORD' })}>
{state.showPassword ? "إخفاء" : "إظهار"}
</button>
<button type="submit" disabled={state.isLoading}>
{state.isLoading ? "جاري التحميل..." : "تسجيل الدخول"}
</button>
{state.error && <p>{state.error}</p>}
</form>
);
}لاحظ كيف أن كل تحديث للحالة يتم عبر dispatch، مما يجعل منطق التحديث مركزياً وسهل التتبع. هذا النمط مفيد جداً عندما يكون لديك حالة معقدة تحتاج إلى تحديثات متعددة ومترابطة. لكن حتى useReducer له حدوده. عندما تحتاج إلى مشاركة الحالة بين مكونات غير مرتبطة ببعضها البعض، أو عندما يصبح الـ reducer كبيراً جداً، فإنك تحتاج إلى حل أكثر قوة.
Context API هو الحل الرسمي من React لمشكلة مشاركة الحالة بين المكونات. الفكرة بسيطة: بدلاً من تمرير الـ props يدوياً عبر كل مكون وسيط، يمكنك إنشاء context وتزويده بقيمة، ثم أي مكون داخل الشجرة يمكنه قراءة هذه القيمة. لكن ما لا يفهمه الكثيرون هو أن Context ليس مجرد بديل لـ props drilling، بل هو أداة لإدارة الحالة العالمية بطريقة أكثر كفاءة من حيث الأداء.
عندما تستخدم Context، فإنك في الواقع تطلب من React إنشاء شجرة فرعية جديدة في الـ Fiber tree. هذا يعني أن أي مكون يقرأ من Context سيُعاد تصييره تلقائياً عندما تتغير القيمة في Context، حتى لو كان هذا المكون بعيداً جداً في الشجرة. هذا السلوك يمكن أن يكون مفيداً، ولكنه قد يتسبب أيضاً في مشاكل أداء إذا لم تستخدمه بحذر. مثلاً، إذا كان لديك Context يحتوي على حالة كبيرة ومتغيرة باستمرار، فإن كل مكون يقرأ من هذا Context سيُعاد تصييره في كل مرة تتغير القيمة، حتى لو كان المكون لا يستخدم الجزء المتغير من الحالة.
// مثال على استخدام Context API مع تقسيم الحالة لتجنب إعادة التصيير غير الضرورية
import { createContext, useContext, useState, useMemo } from 'react';
// تقسيم Context إلى جزئين لتجنب إعادة التصيير غير الضرورية
const UserC createContext();
const ThemeContext = createContext();
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// استخدام useMemo لتجنب إعادة إنشاء القيمة المرجعية في كل تصيير
const userValue = useMemo(() => ({ user, setUser }), [user]);
const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<UserContext.Provider value={userValue}>
<ThemeContext.Provider value={themeValue}>
<Toolbar />
<Content />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
function Toolbar() {
const { theme, setTheme } = useContext(ThemeContext);
// لن يتم إعادة تصيير هذا المكون عند تغيير user
return (
<div style={{ background: theme === 'light' ? '#fff' : '#333' }}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
تبديل الثيم
</button>
</div>
);
}
function Content() {
const { user } = useContext(UserContext);
const { theme } = useContext(ThemeContext);
// سيتم إعادة تصيير هذا المكون عند تغيير user أو theme
return (
<div style={{ color: theme === 'light' ? '#000' : '#fff' }}>
{user ? `مرحباً، ${user.name}` : 'يرجى تسجيل الدخول'}
</div>
);
}في هذا المثال، قمنا بتقسيم Context إلى جزئين منفصلين: UserContext وThemeContext. هذا يعني أن مكون Toolbar لن يُعاد تصييره عند تغيير حالة المستخدم، لأننا فصلنا الحالة إلى جزئين مستقلين. هذه التقنية تسمى Context splitting، وهي ضرورية لتجنب إعادة التصيير غير الضرورية. لكن حتى مع هذه التحسينات، Context API ليس الحل الأمثل لكل الحالات. عندما يصبح التطبيق كبيراً جداً، أو عندما تحتاج إلى منطق تحديث معقد، فإنك قد تحتاج إلى مكتبة خارجية لإدارة الحالة.
Redux هي المكتبة الأشهر لإدارة الحالة في تطبيقات React، لكنها أيضاً الأكثر تعقيداً. الكثير من المطورين يستخدمون Redux فقط لأنها مشهورة، دون أن يفهموا حقاً متى تكون الحاجة إليها ملحة. الحقيقة هي أن Redux ليست الحل الأمثل لكل مشروع. في الواقع، في معظم التطبيقات الصغيرة والمتوسطة، يمكنك الاستغناء عنها تماماً باستخدام Context API وuseReducer.
إذاً متى تصبح الحاجة إلى Redux ملحة؟ هناك ثلاث حالات رئيسية: أولاً، عندما يكون لديك حالة عالمية معقدة تحتاج إلى مشاركة بين مكونات عديدة وغير مرتبطة ببعضها البعض. ثانياً، عندما تحتاج إلى تتبع تاريخ الحالة (time-travel debugging) أو التراجع عن الإجراءات (undo/redo). ثالثاً، عندما تحتاج إلى منطق تحديث معقد يتضمن عمليات غير متزامنة، أو عندما تحتاج إلى دمج بيانات من مصادر متعددة. في هذه الحالات، فإن بنية Redux المكونة من actions، reducers، وstore تصبح مفيدة جداً.
// مثال مبسط على Redux مع middleware للتتبع
import { createStore, applyMiddleware } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
// Action Types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const SET_FILTER = 'SET_FILTER';
// Action Creators
const addTodo = (text) => ({ type: ADD_TODO, text });
const toggleTodo = (id) => ({ type: TOGGLE_TODO, id });
const setFilter = (filter) => ({ type: SET_FILTER, filter });
// Reducer
function todosReducer(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, { id: Date.now(), text: action.text, completed: false }];
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
default:
return state;
}
}
function filterReducer(state = 'SHOW_ALL', action) {
switch (action.type) {
case SET_FILTER:
return action.filter;
default:
return state;
}
}
const rootReducer = (state = {}, action) => {
return {
todos: todosReducer(state.todos, action),
filter: filterReducer(state.filter, action)
};
};
// Middleware للتتبع
const loggerMiddleware = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));
// مكونات React
function TodoApp() {
const todos = useSelector(state => state.todos);
const filter = useSelector(state => state.filter);
const dispatch = useDispatch();
const filteredTodos = todos.filter(todo => {
if (filter === 'SHOW_ALL') return true;
if (filter === 'SHOW_COMPLETED') return todo.completed;
if (filter === 'SHOW_ACTIVE') return !todo.completed;
return true;
});
return (
<div>
<input
type="text"
{(e) => {
if (e.key === 'Enter') {
dispatch(addTodo(e.target.value));
e.target.value = '';
}
}}
/>
<ul>
{filteredTodos.map(todo => (
<li
key={todo.id}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => dispatch(toggleTodo(todo.id))}
>
{todo.text}
</li>
))}
</ul>
<div>
<button onClick={() => dispatch(setFilter('SHOW_ALL'))}>الكل</button>
<button onClick={() => dispatch(setFilter('SHOW_ACTIVE'))}>النشطة</button>
<button onClick={() => dispatch(setFilter('SHOW_COMPLETED'))}>المكتملة</button>
</div>
</div>
);
}
// التطبيق الرئيسي
function App() {
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}في هذا المثال، استخدمنا Redux لإدارة قائمة المهام مع فلتر. لاحظ كيف أن كل تحديث للحالة يتم عبر dispatch، وكيف أن الـ reducer هو دالة خالصة (pure function) لا تسبب أي آثار جانبية. هذا النمط يجعل الكود أكثر قابلية للتنبؤ، وأسهل في الاختبار. لكن Redux لها عيوبها أيضاً. فهي تتطلب كتابة الكثير من الكود التمهيدي، وتجعل من الصعب إدارة الحالة المحلية. بالإضافة إلى ذلك، فإن استخدام Redux مع TypeScript يمكن أن يكون معقداً بسبب الحاجة إلى تعريف أنواع Actions وState.
في رأيي، يجب تجنب Redux في ثلاث حالات رئيسية: أولاً، إذا كان تطبيقك صغيراً ولا يحتاج إلى مشاركة حالة معقدة بين مكونات عديدة. ثانياً، إذا كنت تعمل في فريق صغير أو بمفردك، ولا تحتاج إلى الأدوات المتقدمة التي توفرها Redux مثل time-travel debugging. ثالثاً، إذا كنت تريد تبسيط الكود وتقليل حجم الحزمة النهائية للتطبيق. في هذه الحالات، يمكنك استخدام Context API مع useReducer، أو حتى مكتبة أخف مثل Zustand أو Jotai.
في السنوات الأخيرة، ظهرت مكتبات جديدة لإدارة الحالة في React تحاول حل المشاكل التي تعاني منها Redux، مع الحفاظ على بساطة useState وContext API. من بين هذه المكتبات، تبرز حالتان: Zustand وJotai. كلاهما خفيف الوزن، سهل الاستخدام، ولا يتطلب كتابة الكثير من الكود التمهيدي.
Zustand هي مكتبة لإدارة الحالة العالمية تعتمد على نمط مشابه لـ Redux، لكنها أبسط بكثير. بدلاً من استخدام actions وreducers، يمكنك إنشاء store يحتوي على الحالة والدوال لتحديثها مباشرة. Zustand تستخدم الـ closure في JavaScript لتوفير واجهة برمجية بسيطة، ولكنها تحتفظ بالقدرة على تتبع التحديثات وإعادة التصيير بكفاءة. أحد المزايا الرئيسية لـ Zustand هي أنها لا تتطلب استخدام Provider، مما يجعلها أسهل في الإعداد والاستخدام في التطبيقات الكبيرة.
// مثال على استخدام Zustand لإدارة حالة عربة التسوق
import create from 'zustand';
// إنشاء store
const useStore = create((set) => ({
cart: [],
addToCart: (product) => set((state) => ({
cart: [...state.cart, product]
})),
removeFromCart: (productId) => set((state) => ({
cart: state.cart.filter(item => item.id !== productId)
})),
clearCart: () => set({ cart: [] }),
total: 0,
calculateTotal: () => set((state) => ({
total: state.cart.reduce((sum, item) => sum + item.price, 0)
}))
}));
// مكون React
function Cart() {
const { cart, addToCart, removeFromCart, total, calculateTotal } = useStore();
// حساب الإجمالي عند تغيير العربة
React.useEffect(() => {
calculateTotal();
}, [cart, calculateTotal]);
return (
<div>
<h2>عربة التسوق</h2>
<ul>
{cart.map(item => (
<li key={item.id}>
{item.name} - {item.price} ريال
<button {() => removeFromCart(item.id)}>إزالة</button>
</li>
))}
</ul>
<p>الإجمالي: {total} ريال</p>
<button onClick={() => addToCart({ id: Date.now(), name: 'منتج جديد', price: 50 })}>
إضافة منتج
</button>
</div>
);
}في هذا المثال، استخدمنا Zustand لإنشاء store لإدارة عربة التسوق. لاحظ كيف أننا لم نحتاج إلى Provider أو كتابة الكثير من الكود التمهيدي. Zustand تتعامل مع إعادة التصيير بكفاءة، حيث أنها تستخدم تقنية تسمى selective subscriptions، مما يعني أن المكونات لن تُعاد تصييرها إلا إذا تغير الجزء من الحالة الذي تستخدمه.
أما Jotai فهي مكتبة مختلفة تماماً. بدلاً من استخدام store واحد كبير، Jotai تسمح لك بإنشاء ذرات (atoms) صغيرة من الحالة، يمكن دمجها معاً عند الحاجة. هذا النمط يشبه إلى حد كبير كيفية عمل Recoil من فيسبوك، ولكنه أخف وزناً وأكثر بساطة. Jotai مفيدة جداً عندما يكون لديك حالة موزعة في أماكن متعددة من التطبيق، وتحتاج إلى مشاركة أجزاء صغيرة منها بين المكونات دون الحاجة إلى store مركزي.
// مثال على استخدام Jotai لإدارة حالة المستخدم والثيم
import { atom, useAtom } from 'jotai';
// تعريف ذرات الحالة
const userAtom = atom(null);
const themeAtom = atom('light');
const isLoggedInAtom = atom((get) => !!get(userAtom));
// مكونات React
function UserProfile() {
const [user, setUser] = useAtom(userAtom);
const [isLoggedIn] = useAtom(isLoggedInAtom);
return (
<div>
{isLoggedIn ? (
<div>
<p>مرحباً، {user.name}</p>
<button {() => setUser(null)}>تسجيل الخروج</button>
</div>
) : (
<button onClick={() => setUser({ name: 'محمد', id: 1 })}>تسجيل الدخول</button>
)}
</div>
);
}
function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
تبديل الثيم ({theme})
</button>
);
}
function App() {
return (
<div>
<UserProfile />
<ThemeToggle />
</div>
);
}في هذا المثال، استخدمنا Jotai لإنشاء ذرات منفصلة لحالة المستخدم والثيم. لاحظ كيف أن isLoggedInAtom هو ذرة مشتقة تعتمد على userAtom. هذا النمط يجعل من السهل إدارة الحالة الموزعة، حيث أن كل مكون يمكنه الوصول فقط إلى الذرات التي يحتاجها، دون الحاجة إلى إعادة تصيير عند تغيير أجزاء أخرى من الحالة.
في رأيي، Zustand هي الخيار الأفضل عندما يكون لديك حالة عالمية واحدة تحتاج إلى مشاركتها بين مكونات عديدة، وعندما تريد تبسيط الكود مقارنةً بـ Redux. أما Jotai فهي الخيار الأفضل عندما يكون لديك حالة موزعة في أماكن متعددة، وتحتاج إلى مشاركة أجزاء صغيرة منها بين المكونات دون الحاجة إلى store مركزي. كلتا المكتبتين خفيفتان وسهلتان الاستخدام، وهما بديلان رائعان لـ Redux في معظم الحالات.
إدارة الحالة غير المتزامنة هي واحدة من أصعب التحديات في تطوير تطبيقات React. عندما تتعامل مع بيانات تأتي من API، أو عمليات طويلة الأمد مثل رفع الملفات، فإنك تحتاج إلى طريقة لإدارة الحالة التي تتغير بمرور الوقت. هناك عدة أدوات لحل هذه المشكلة، وكل منها له استخداماته الخاصة.
أبسط أداة لإدارة الحالة غير المتزامنة هي useEffect مع useState. يمكنك استخدام useEffect لجلب البيانات عند تحميل المكون، واستخدام useState لتخزين البيانات والحالة (loading، error، data). هذا النمط كافٍ تماماً للتطبيقات الصغيرة، ولكنه يصبح صعب الإدارة في التطبيقات الكبيرة. المشكلة الرئيسية هنا هي أنك تحتاج إلى تكرار نفس الكود في كل مكون يحتاج إلى جلب البيانات، مما يؤدي إلى تكرار الكود وصعوبة الصيانة.
// مثال على جلب البيانات باستخدام useEffect وuseState
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const resp await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('فشل جلب البيانات');
}
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <p>جاري التحميل...</p>;
if (error) return <p>خطأ: {error}</p>;
if (!user) return <p>لا توجد بيانات</p>;
return (
<div>
<h2>{user.name}</h2>
<p>البريد الإلكتروني: {user.email}</p>
</div>
);
}هذا النمط يعمل بشكل جيد للتطبيقات الصغيرة، ولكنه يصبح صعب الإدارة عندما تحتاج إلى جلب البيانات في مكونات متعددة، أو عندما تحتاج إلى مزامنة البيانات بين المكونات. هنا تأتي الحاجة إلى مكتبات متخصصة لإدارة الحالة غير المتزامنة، مثل React Query أو SWR.
React Query هي مكتبة متخصصة لإدارة البيانات غير المتزامنة في React. بدلاً من كتابة useEffect وuseState يدوياً، يمكنك استخدام React Query لجلب البيانات، وتخزينها مؤقتاً، وتحديثها تلقائياً عند الحاجة. React Query توفر أيضاً ميزات متقدمة مثل إعادة المحاولة التلقائية، وتحديث البيانات في الخلفية، ومزامنة البيانات بين المكونات.
// مثال على استخدام React Query لجلب البيانات
import { useQuery } from 'react-query';
async function fetchUser(userId) {
const resp await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('فشل جلب البيانات');
}
return response.json();
}
function UserProfile({ userId }) {
const { data: user, isLoading, error } = useQuery(['user', userId], () => fetchUser(userId));
if (isLoading) return <p>جاري التحميل...</p>;
if (error) return <p>خطأ: {error.message}</p>;
return (
<div>
<h2>{user.name}</h2>
<p>البريد الإلكتروني: {user.email}</p>
</div>
);
}
// في التطبيق الرئيسي
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserProfile userId={1} />
</QueryClientProvider>
);
}في هذا المثال، استخدمنا React Query لجلب بيانات المستخدم. لاحظ كيف أن الكود أصبح أبسط وأكثر قابلية للصيانة. React Query تتعامل مع التخزين المؤقت، وإعادة المحاولة، وتحديث البيانات تلقائياً، مما يجعلها أداة قوية لإدارة البيانات غير المتزامنة. لكن React Query ليست الحل الأمثل لكل الحالات. إذا كان لديك حالة غير متزامنة معقدة تحتاج إلى مشاركتها بين مكونات عديدة، فإنك قد تحتاج إلى دمجها مع مكتبة لإدارة الحالة مثل Redux أو Zustand.
إذا كنت تستخدم بالفعل Redux في تطبيقك، فإن Redux Toolkit Query هي خيار جيد لإدارة البيانات غير المتزامنة. Redux Toolkit Query هي جزء من Redux Toolkit، وهي توفر واجهة برمجية مشابهة لـ React Query، ولكنها مدمجة مع Redux. هذا يعني أنك يمكنك إدارة البيانات غير المتزامنة والحالة العامة للتطبيق في مكان واحد، مما يبسط الكود ويقلل من الحاجة إلى مكتبات إضافية.
// مثال على استخدام Redux Toolkit Query
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';
// تعريف API
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
endpoints: (builder) => ({
getUser: builder.query({
query: (userId) => `users/${userId}`,
}),
}),
});
// تعريف slice للحالة العامة
const authSlice = createSlice({
name: 'auth',
initialState: { user: null },
reducers: {
setUser: (state, action) => {
state.user = action.payload;
},
},
});
// إنشاء store
const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
auth: authSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
});
// مكونات React
function UserProfile({ userId }) {
const { data: user, isLoading, error } = api.useGetUserQuery(userId);
if (isLoading) return <p>جاري التحميل...</p>;
if (error) return <p>خطأ: {error.error}</p>;
return (
<div>
<h2>{user.name}</h2>
<p>البريد الإلكتروني: {user.email}</p>
</div>
);
}
function App() {
return (
<Provider store={store}>
<UserProfile userId={1} />
</Provider>
);
}في هذا المثال، استخدمنا Redux Toolkit Query لجلب بيانات المستخدم. لاحظ كيف أن الكود أصبح أكثر تنظيماً، حيث أن إدارة البيانات غير المتزامنة والحالة العامة للتطبيق تتم في مكان واحد. هذا النمط مفيد جداً للتطبيقات الكبيرة التي تستخدم Redux بالفعل، حيث أنه يقلل من الحاجة إلى مكتبات إضافية ويبسط الكود.
بعد كل هذا الشرح، قد تشعر بالحيرة: أي أداة تختار؟ الحقيقة هي أنه لا توجد أداة مثالية لكل الحالات. الاختيار يعتمد على حجم التطبيق، وتعقيد الحالة، واحتياجات الفريق. لكن إليك خارطة طريق بسيطة تساعدك على اتخاذ القرار:
في النهاية، لا تجعل الاختيار معقداً أكثر مما يجب. ابدأ بالحلول البسيطة، وانتقل إلى الحلول الأكثر تعقيداً فقط عندما تصبح الحاجة ملحة. تذكر أن الهدف من إدارة الحالة هو جعل الكود أكثر قابلية للصيانة وسهولة التوسع، وليس تعقيده أكثر.
إذا كنت ستبدأ مشروعاً جديداً اليوم، فإليك ما سأفعله أنا: سأستخدم Zustand لإدارة الحالة العامة، وReact Query لإدارة البيانات غير المتزامنة. هذا المزيج يوفر لي بساطة useState، وقوة Redux، وإمكانيات React Query لإدارة البيانات غير المتزامنة، دون الحاجة إلى كتابة الكثير من الكود التمهيدي أو التعامل مع تعقيدات Redux. وإذا كان التطبيق صغيراً، فسأكتفي بـ Context API وuseReducer. القاعدة الذهبية هي: ابدأ بالبسيط، وانتقل إلى المعقد فقط عندما تحتاج إليه فعلاً.