أساسيات React.js

useReducer Hook - إدارة الحالة المعقدة

18 دقيقة الدرس 14 من 40

فهم useReducer

useReducer hook هو بديل لـ useState لإدارة منطق الحالة المعقدة. إنه مشابه لكيفية عمل Redux - تقوم بإرسال إجراءات إلى دالة مخفض، والتي تحسب الحالة التالية بناءً على الحالة الحالية والإجراء.

useReducer مفيد بشكل خاص عندما يكون لديك منطق حالة معقد يتضمن قيمًا فرعية متعددة، عندما تعتمد الحالة التالية على السابقة، أو عندما تريد تحسين الأداء للمكونات التي تؤدي إلى تحديثات عميقة.

متى تستخدم useReducer

استخدم useReducer عندما: تتضمن تحديثات الحالة منطقًا معقدًا، تتغير قيم حالة متعددة مرتبطة معًا، تعتمد الحالة التالية على الحالة السابقة، تحتاج إلى انتقالات حالة يمكن التنبؤ بها، أو تريد فصل منطق الحالة عن كود المكون.

صيغة useReducer الأساسية

يأخذ useReducer hook ثلاث وسائط: دالة مخفض، وحالة أولية، ودالة init اختيارية. يُرجع الحالة الحالية ودالة dispatch.

import React, { useReducer } from 'react';

// دالة المخفض: (currentState, action) => newState
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    default:
      throw new Error(`نوع إجراء غير معروف: ${action.type}`);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <h2>العدد: {state.count}</h2>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>
        +1
      </button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>
        -1
      </button>
      <button onClick={() => dispatch({ type: 'RESET' })}>
        إعادة تعيين
      </button>
    </div>
  );
}

export default Counter;

إجراءات مع حمولات

يمكن للإجراءات حمل بيانات إضافية (حمولة) إلى المخفض:

function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'ADD':
      return { count: state.count + action.payload };
    case 'SUBTRACT':
      return { count: state.count - action.payload };
    case 'SET':
      return { count: action.payload };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <h2>العدد: {state.count}</h2>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>
        +1
      </button>
      <button onClick={() => dispatch({ type: 'ADD', payload: 5 })}>
        +5
      </button>
      <button onClick={() => dispatch({ type: 'ADD', payload: 10 })}>
        +10
      </button>
      <button onClick={() => dispatch({ type: 'SET', payload: 0 })}>
        إعادة تعيين
      </button>
    </div>
  );
}

أفضل ممارسات المخفض

ارجع دائمًا كائن حالة جديد (لا تغير)، استخدم أنواع إجراءات وصفية (ثوابت)، تعامل مع جميع الحالات أو أرجع الحالة الحالية في default، واحتفظ بالمخفضات نقية (بدون تأثيرات جانبية).

مثال حالة معقدة: قائمة المهام

إدارة قائمة مهام بعمليات متعددة:

import React, { useReducer, useState } from 'react';

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: Date.now(),
            text: action.payload,
            completed: false
          }
        ]
      };

    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };

    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      };

    case 'EDIT_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, text: action.payload.text }
            : todo
        )
      };

    case 'CLEAR_COMPLETED':
      return {
        ...state,
        todos: state.todos.filter(todo => !todo.completed)
      };

    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, { todos: [] });
  const [inputValue, setInputValue] = useState('');

  const addTodo = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      dispatch({ type: 'ADD_TODO', payload: inputValue });
      setInputValue('');
    }
  };

  return (
    <div>
      <h2>قائمة المهام</h2>
      <form onSubmit={addTodo}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="أضف مهمة..."
        />
        <button type="submit">إضافة</button>
      </form>

      <ul>
        {state.todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
              حذف
            </button>
          </li>
        ))}
      </ul>

      <button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}>
        مسح المكتملة
      </button>
    </div>
  );
}

export default TodoApp;

useReducer مقابل useState

الاختيار بين useReducer و useState:

// useState - حالة بسيطة
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

// useReducer - حالة معقدة بعمليات متعددة
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>إعادة تعيين</button>
    </div>
  );
}

خطأ شائع: تغيير الحالة

لا تغير الحالة مباشرة في مخفض أبدًا. ارجع دائمًا كائن حالة جديد. استخدم عوامل النشر أو طرق مثل map() و filter() التي ترجع مصفوفات جديدة.

التهيئة الكسولة

استخدم دالة init لحسابات الحالة الأولية المكلفة:

function init(initialCount) {
  // حساب مكلف
  return { count: initialCount * 2 };
}

function Counter({ initialCount }) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);

  // يتم استدعاء دالة init مرة واحدة فقط عند التحميل
  return <div>العدد: {state.count}</div>;
}

إدارة النماذج مع useReducer

إدارة حالة نموذج معقدة:

import React, { useReducer } from 'react';

function formReducer(state, action) {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        values: {
          ...state.values,
          [action.field]: action.value
        }
      };

    case 'SET_ERROR':
      return {
        ...state,
        errors: {
          ...state.errors,
          [action.field]: action.error
        }
      };

    case 'SET_SUBMITTING':
      return {
        ...state,
        isSubmitting: action.value
      };

    case 'RESET_FORM':
      return action.payload;

    default:
      return state;
  }
}

function RegistrationForm() {
  const initialState = {
    values: {
      username: '',
      email: '',
      password: ''
    },
    errors: {},
    isSubmitting: false
  };

  const [state, dispatch] = useReducer(formReducer, initialState);

  const handleChange = (field) => (e) => {
    dispatch({
      type: 'SET_FIELD',
      field,
      value: e.target.value
    });
  };

  const validate = () => {
    const newErrors = {};

    if (!state.values.username) {
      newErrors.username = 'اسم المستخدم مطلوب';
    }

    if (!state.values.email.includes('@')) {
      newErrors.email = 'بريد إلكتروني غير صالح';
    }

    if (state.values.password.length < 6) {
      newErrors.password = 'يجب أن تكون كلمة المرور 6 أحرف على الأقل';
    }

    return newErrors;
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    const errors = validate();

    if (Object.keys(errors).length > 0) {
      Object.entries(errors).forEach(([field, error]) => {
        dispatch({ type: 'SET_ERROR', field, error });
      });
      return;
    }

    dispatch({ type: 'SET_SUBMITTING', value: true });

    try {
      // محاكاة استدعاء API
      await new Promise(resolve => setTimeout(resolve, 1000));
      console.log('تم إرسال النموذج:', state.values);
      dispatch({ type: 'RESET_FORM', payload: initialState });
    } catch (error) {
      console.error('خطأ في الإرسال:', error);
    } finally {
      dispatch({ type: 'SET_SUBMITTING', value: false });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>اسم المستخدم:</label>
        <input
          type="text"
          value={state.values.username}
          onChange={handleChange('username')}
        />
        {state.errors.username && <span>{state.errors.username}</span>}
      </div>

      <div>
        <label>البريد الإلكتروني:</label>
        <input
          type="email"
          value={state.values.email}
          onChange={handleChange('email')}
        />
        {state.errors.email && <span>{state.errors.email}</span>}
      </div>

      <div>
        <label>كلمة المرور:</label>
        <input
          type="password"
          value={state.values.password}
          onChange={handleChange('password')}
        />
        {state.errors.password && <span>{state.errors.password}</span>}
      </div>

      <button type="submit" disabled={state.isSubmitting}>
        {state.isSubmitting ? 'جاري الإرسال...' : 'التسجيل'}
      </button>
    </form>
  );
}

export default RegistrationForm;

الجمع بين useReducer و Context

أنشئ حل إدارة حالة عام من خلال الجمع بين useReducer و Context:

import React, { createContext, useContext, useReducer } from 'react';

// الإجراءات
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';

// المخفض
function counterReducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    case RESET:
      return { count: 0 };
    default:
      return state;
  }
}

// السياق
const CounterContext = createContext();

// الموفر
export function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
}

// Hook مخصص
export function useCounter() {
  const context = useContext(CounterContext);
  if (!context) {
    throw new Error('useCounter must be used within CounterProvider');
  }
  return context;
}

// الاستخدام في المكونات
function CounterDisplay() {
  const { state } = useCounter();
  return <h2>العدد: {state.count}</h2>;
}

function CounterButtons() {
  const { dispatch } = useCounter();
  return (
    <div>
      <button onClick={() => dispatch({ type: INCREMENT })}>+</button>
      <button onClick={() => dispatch({ type: DECREMENT })}>-</button>
      <button onClick={() => dispatch({ type: RESET })}>إعادة تعيين</button>
    </div>
  );
}

function App() {
  return (
    <CounterProvider>
      <CounterDisplay />
      <CounterButtons />
    </CounterProvider>
  );
}

نمط مخفضات متعددة

استخدم مخفضات متعددة لأجزاء مختلفة من تطبيقك:

function App() {
  const [userState, userDispatch] = useReducer(userReducer, initialUserState);
  const [cartState, cartDispatch] = useReducer(cartReducer, initialCartState);
  const [uiState, uiDispatch] = useReducer(uiReducer, initialUIState);

  return (
    <UserContext.Provider value={{ userState, userDispatch }}>
      <CartContext.Provider value={{ cartState, cartDispatch }}>
        <UIContext.Provider value={{ uiState, uiDispatch }}>
          <AppContent />
        </UIContext.Provider>
      </CartContext.Provider>
    </UserContext.Provider>
  );
}

نمط منشئي الإجراءات

أنشئ دوال مساعدة لتوليد الإجراءات:

// منشئي الإجراءات
const actions = {
  addTodo: (text) => ({ type: 'ADD_TODO', payload: text }),
  toggleTodo: (id) => ({ type: 'TOGGLE_TODO', payload: id }),
  deleteTodo: (id) => ({ type: 'DELETE_TODO', payload: id }),
  editTodo: (id, text) => ({ type: 'EDIT_TODO', payload: { id, text } })
};

// الاستخدام
function TodoList() {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  return (
    <div>
      <button onClick={() => dispatch(actions.addTodo('مهمة جديدة'))}>
        إضافة مهمة
      </button>
      {state.todos.map(todo => (
        <div key={todo.id}>
          <span onClick={() => dispatch(actions.toggleTodo(todo.id))}>
            {todo.text}
          </span>
          <button onClick={() => dispatch(actions.deleteTodo(todo.id))}>
            حذف
          </button>
        </div>
      ))}
    </div>
  );
}

تمرين 1: عربة التسوق

قم ببناء عربة تسوق باستخدام useReducer تدعم الإضافة والإزالة وتحديث الكميات.

// المتطلبات:
// 1. إضافة عنصر إلى العربة (أو الزيادة إذا كان موجودًا)
// 2. إزالة عنصر من العربة
// 3. تحديث كمية العنصر
// 4. مسح العربة بأكملها
// 5. عرض السعر الإجمالي وعدد العناصر

تمرين 2: نموذج متعدد الخطوات

أنشئ نموذج تسجيل متعدد الخطوات مع useReducer يدير حالة النموذج والتنقل.

// المتطلبات:
// 1. ثلاث خطوات: معلومات شخصية، تفاصيل الحساب، التأكيد
// 2. أزرار التالي/السابق/الإرسال
// 3. التحقق من كل خطوة قبل المتابعة
// 4. عرض الأخطاء لكل حقل
// 5. عرض الملخص في خطوة التأكيد

تمرين 3: تطبيق اختبار

قم ببناء تطبيق اختبار مع useReducer يدير الأسئلة والإجابات والنتيجة والتنقل.

// المتطلبات:
// 1. عرض سؤال واحد في كل مرة
// 2. تتبع الإجابات المحددة
// 3. حساب النتيجة
// 4. عرض النتائج في النهاية
// 5. وظيفة إعادة تعيين الاختبار

الملخص

  • useReducer يدير الحالة المعقدة بتحديثات يمكن التنبؤ بها عبر الإجراءات
  • دالة المخفض: (state, action) => newState
  • استخدم لمنطق الحالة المعقدة، أو القيم المرتبطة المتعددة، أو عندما تعتمد الحالة التالية على السابقة
  • يمكن للإجراءات حمل حمولات لبيانات إضافية
  • ارجع دائمًا كائنات حالة جديدة (تحديثات ثابتة)
  • اجمع مع Context لإدارة الحالة العامة
  • استخدم منشئي الإجراءات لاستدعاءات dispatch أنظف
  • التهيئة الكسولة لحسابات الحالة الأولية المكلفة