تعلّم React Hooks خطوة بخطوة

تعلّم React Hooks خطوة بخطوة

إذا كنت قد بدأت رحلتك مع React، فغالبًا مررت بهذه اللحظة: ترى مكوّنات وظيفية صغيرة، ثم فجأة يظهر لك عالم اسمه Hooks، وتسمع عن useState وuseEffect وuseMemo وuseRef وكأنها مفاتيح سحرية تفتح لك أبواب بناء واجهات حديثة وسريعة ومنظمة.
في البداية قد يبدو الأمر مربكًا قليلًا، لكن الجميل في React Hooks أنه بمجرد أن تفهم الفكرة الأساسية، ستجد نفسك تكتب كودًا أنظف، وأقل تعقيدًا، وأكثر قابلية لإعادة الاستخدام.

هذا المقال ليس مجرد شرح نظري. هو رحلة عملية مريحة ومفصلة، من الأساسيات إلى الاستخدامات المتقدمة، مع أمثلة برمجية واضحة، ونصائح من واقع التجربة، وأخطاء شائعة يقع فيها الكثيرون، وكيف تتجنبها. سنحاول أن نجعل التعلم خفيفًا، وكأننا نجلس معًا نكتب أول مشروع React Hooks من الصفر.


ما هي React Hooks أصلًا؟

React Hooks هي دوال أُضيفت إلى React تسمح لك باستخدام ميزات React داخل المكوّنات الوظيفية بدلًا من الاعتماد فقط على Class Components.

قبل ظهور Hooks، كان المطورون يعتمدون كثيرًا على الكلاسات لإدارة الحالة (state) وربط الأحداث الجانبية (side effects).
بعد Hooks أصبح بإمكانك تنفيذ معظم ذلك داخل دوال عادية، وهذا غيّر طريقة كتابة تطبيقات React بشكل كبير.

ببساطة:

  • useState لإدارة الحالة.

  • useEffect لتنفيذ الأوامر الجانبية.

  • useRef للاحتفاظ بقيمة ثابتة أو الوصول إلى عنصر DOM.

  • useMemo لتخزين ناتج حساب مكلف.

  • useCallback لتخزين دالة وعدم إعادة إنشائها دون حاجة.

  • Hooks مخصصة لتجميع المنطق وإعادة استخدامه.

الفكرة المهمة هنا ليست أن Hooks “أحدث” فقط، بل أنها تساعدك على فصل المنطق عن العرض بطريقة أسهل بكثير.


لماذا Hooks مهمة جدًا؟

لأنها تجعل الكود:

  • أبسط في القراءة.

  • أسهل في إعادة الاستخدام.

  • أقل ازدحامًا من الكلاسات.

  • أكثر مرونة عند بناء مشاريع كبيرة.

  • أفضل في تنظيم المنطق المعقد.

تخيل أنك تريد بناء صفحة فيها عداد، ونموذج إدخال، وتفاعل مع API، وتحديثات تلقائية.
في الكلاسات، قد تبدأ الأمور بالتشابك.
مع Hooks، يمكنك تقسيم المنطق إلى أجزاء صغيرة وواضحة.


أول Hook: useState

ما وظيفته؟

useState هو Hook مسؤول عن إدارة الحالة داخل المكوّن.

عندما تتغير الحالة، يعيد React رسم المكوّن تلقائيًا.

الشكل العام

import React, { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>العدد الحالي: {count}</p>
      <button onClick={() => setCount(count + 1)}>زيادة</button>
    </div>
  );
}

export default Counter;

كيف نفهم هذا؟

const [count, setCount] = useState(0);
  • count: القيمة الحالية.

  • setCount: الدالة التي تغيّر القيمة.

  • useState(0): القيمة الابتدائية هي 0.

مثال بسيط أكثر وضوحًا

import React, { useState } from "react";

function LikeButton() {
  const [liked, setLiked] = useState(false);

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? "تم الإعجاب" : "أعجبني"}
    </button>
  );
}

export default LikeButton;

هنا نبدّل بين true و false.
هذا النمط يتكرر كثيرًا في التطبيقات الحقيقية.


تحديث الحالة بطريقة صحيحة

في بعض الحالات، تعتمد القيمة الجديدة على القيمة القديمة. هنا من الأفضل استخدام الشكل الوظيفي:

setCount(prevCount => prevCount + 1);

بدلًا من:

setCount(count + 1);

متى يهم هذا؟

عندما تنفذ تحديثات متتابعة أو تعتمد على حالة سابقة قد تتغير بسرعة.

مثال:

import React, { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  const increaseThreeTimes = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={increaseThreeTimes}>+3</button>
    </div>
  );
}

هذا أكثر أمانًا من الاعتماد على قيمة count مباشرة في كل مرة.


إدارة الأشكال Forms باستخدام useState

من أكثر الأشياء شيوعًا في React إدارة النماذج.

مثال نموذج بسيط

import React, { useState } from "react";

function ContactForm() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    alert(`الاسم: ${name}\nالبريد: ${email}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>الاسم:</label>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
      </div>

      <div>
        <label>البريد الإلكتروني:</label>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </div>

      <button type="submit">إرسال</button>
    </form>
  );
}

export default ContactForm;

ملاحظات مهمة

  • value يجعل الحقل controlled component.

  • onChange يربط الإدخال بالحالة.

  • e.preventDefault() يمنع إعادة تحميل الصفحة.


useState مع كائنات Objects

أحيانًا من الأفضل تخزين البيانات داخل كائن واحد.

import React, { useState } from "react";

function ProfileForm() {
  const [form, setForm] = useState({
    name: "",
    age: "",
  });

  const handleChange = (e) => {
    const { name, value } = e.target;

    setForm((prevForm) => ({
      ...prevForm,
      [name]: value,
    }));
  };

  return (
    <div>
      <input
        name="name"
        value={form.name}
        onChange={handleChange}
        placeholder="الاسم"
      />
      <input
        name="age"
        value={form.age}
        onChange={handleChange}
        placeholder="العمر"
      />
      <p>الاسم: {form.name}</p>
      <p>العمر: {form.age}</p>
    </div>
  );
}

export default ProfileForm;

لماذا نستخدم ...prevForm؟

لأن setState لا يدمج الكائن تلقائيًا مثل class components.
إذا نسيت النسخ الجزئي، قد تفقد بقية القيم.


useEffect: الصديق الذي يتعامل مع الأمور الجانبية

إذا كان useState مسؤولًا عن البيانات، فإن useEffect مسؤول غالبًا عن الأشياء التي تحدث خارج عملية العرض نفسها.

مثل:

  • جلب البيانات من API.

  • الاشتراك في حدث.

  • تغيير عنوان الصفحة.

  • التعامل مع localStorage.

  • تنظيف المؤقتات أو subscriptions.

الشكل الأساسي

import React, { useEffect } from "react";

function Example() {
  useEffect(() => {
    console.log("تم تشغيل المكوّن");

    return () => {
      console.log("تم تنظيف المكوّن");
    };
  }, []);

  return <p>مرحبًا</p>;
}

معنى []

هذه مصفوفة الاعتماديات dependency array.

  • إذا كانت فارغة []، فـ useEffect يعمل مرة واحدة عند التحميل، ثم cleanup عند الإزالة.

  • إذا وضعت متغيرات داخلها، فإن effect يعمل كلما تغيرت تلك المتغيرات.

  • إذا لم تضع شيئًا، يعمل بعد كل render.


جلب البيانات من API باستخدام useEffect

هذا من أكثر الاستخدامات شيوعًا.

import React, { useEffect, useState } from "react";

function UsersList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        const response = await fetch("https://jsonplaceholder.typicode.com/users");
        if (!response.ok) {
          throw new Error("فشل جلب البيانات");
        }
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) return <p>جاري التحميل...</p>;
  if (error) return <p>حدث خطأ: {error}</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default UsersList;

ماذا تعلمنا هنا؟

  • نستخدم حالة للبيانات.

  • نستخدم حالة للتحميل.

  • نستخدم حالة للخطأ.

  • نستخدم try/catch/finally.

  • نضع [] حتى لا يتكرر الجلب بلا نهاية.


التحذير الكبير: لا تجعل useEffect يسبب حلقات لا نهائية

أحد أكثر الأخطاء شيوعًا هو تحديث state داخل useEffect بطريقة تؤدي إلى إعادة تشغيله باستمرار.

مثال خاطئ

useEffect(() => {
  setCount(count + 1);
}, [count]);

هذا سيؤدي إلى حلقة لا نهائية تقريبًا، لأن count يتغير، فيعيد تشغيل effect، فيتغير مرة أخرى، وهكذا.

متى تستخدمه؟

استخدمه فقط عندما تكون متأكدًا أن تغيير الاعتماديات منطقي.


useEffect والتنظيف Cleanup

أحيانًا تحتاج إلى تنظيف شيء بعد انتهاء المكوّن.

مثال مع setInterval

import React, { useEffect, useState } from "react";

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <p>الوقت: {seconds} ثانية</p>;
}

export default Timer;

لماذا cleanup مهم؟

لأنه يمنع:

  • تسريب الذاكرة.

  • استمرار المؤقت بعد إزالة المكوّن.

  • سلوك غير متوقع في التطبيق.


useRef: مرجع ثابت لا يسبب إعادة render

useRef له استخدامات جميلة جدًا.

أهم وظيفتين له

  1. الوصول إلى DOM element.

  2. الاحتفاظ بقيمة لا تحتاج إلى إعادة render عند تغييرها.

مثال للوصول إلى input

import React, { useRef } from "react";

function FocusInput() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="اكتب هنا" />
      <button onClick={handleFocus}>تركيز على الحقل</button>
    </div>
  );
}

export default FocusInput;

لماذا نحتاجه؟

لأن أحيانًا لا نريد إدارة كل شيء بالحالة.
أحيانًا نريد فقط أن نقول: “اذهب إلى هذا العنصر الآن”.


useRef كذاكرة صغيرة داخل المكوّن

import React, { useEffect, useRef, useState } from "react";

function RenderCounter() {
  const [value, setValue] = useState("");
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
  });

  return (
    <div>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <p>عدد مرات الرسم: {renderCount.current}</p>
    </div>
  );
}

export default RenderCounter;

لكن انتبه: useRef.current لا يسبب render عند تغييره.
لهذا هو ممتاز للقيم التي تريد حفظها داخليًا دون التأثير على الواجهة مباشرة.


useMemo: عندما يكون لديك حساب مكلف

useMemo يحفظ ناتج دالة حسابية حتى لا يعيد حسابها إلا عند تغيّر الاعتماديات.

مثال بسيط

import React, { useMemo, useState } from "react";

function ExpensiveCalculation() {
  const [number, setNumber] = useState(1);
  const [theme, setTheme] = useState("light");

  const doubled = useMemo(() => {
    console.log("إعادة حساب القيمة...");
    return number * 2;
  }, [number]);

  return (
    <div>
      <p>القيمة: {doubled}</p>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(Number(e.target.value))}
      />
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        تغيير الثيم
      </button>
    </div>
  );
}

export default ExpensiveCalculation;

الفكرة

عندما تغيّر theme، لا نريد إعادة حساب doubled لأن number لم يتغير.

متى تستخدم useMemo؟

  • عندما يكون لديك حساب ثقيل فعلاً.

  • عندما تريد تقليل إعادة الحسابات غير الضرورية.

  • عندما يكون الأداء مهمًا في مكوّنات كبيرة.

متى لا تحتاجه؟

إذا كان الحساب بسيطًا جدًا، فقد يكون useMemo مجرد تعقيد زائد.


useCallback: حفظ الدوال نفسها

useCallback قريب من useMemo لكن بدل تخزين قيمة، يخزن دالة.

مثال

import React, { useCallback, useState } from "react";

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("تم الضغط");
  }, []);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>زيادة</button>
      <Child onClick={handleClick} />
    </div>
  );
}

function Child({ onClick }) {
  console.log("Child rendered");
  return <button onClick={onClick}>زر الطفل</button>;
}

export default Parent;

لماذا هذا مفيد؟

عند تمرير دوال إلى مكوّنات الأبناء، قد تؤدي إعادة إنشاء الدالة في كل render إلى إعادة render غير ضرورية.

لكن لا تفرط في استخدامه.
في كثير من الحالات، React تعمل بشكل ممتاز دون الحاجة إليه.


الفرق بين useMemo و useCallback

ببساطة:

  • useMemo يحفظ نتيجة.

  • useCallback يحفظ دالة.

مثال ذهني

const result = useMemo(() => expensiveFunction(x), [x]);
const handler = useCallback(() => doSomething(x), [x]);

ترتيب Hooks مهم جدًا

React Hooks لها قاعدة ذهبية:

يجب أن تُستدعى Hooks في نفس الترتيب دائمًا، وفي المستوى الأعلى من المكوّن أو الـ Custom Hook.

لا تفعل هذا

if (condition) {
  useEffect(() => {
    console.log("خطأ");
  }, []);
}

هذا غير صحيح لأن عدد وترتيب استدعاءات Hooks قد يتغير بين renders.

افعل هذا بدلًا منه

useEffect(() => {
  if (condition) {
    console.log("صحيح");
  }
}, [condition]);

قواعد Hooks الأساسية

هناك قاعدتان مهمتان:

  1. لا تستدعِ Hooks داخل الحلقات أو الشروط أو الدوال الداخلية العادية.

  2. استدعِ Hooks فقط داخل React function components أو custom hooks.

هذه القواعد مهمة جدًا حتى يبقى React قادرًا على تتبع الحالة بشكل صحيح.


بناء Custom Hook

الجزء الجميل فعلًا يبدأ هنا.
عندما يتكرر منطق معين في أكثر من مكوّن، يمكنك تحويله إلى Custom Hook.

ما هو Custom Hook؟

هو دالة JavaScript تبدأ غالبًا بـ use وتحتوي على Hooks بداخلها، بهدف إعادة استخدام المنطق.

مثال: Hook لإدارة localStorage

import { useEffect, useState } from "react";

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const storedValue = localStorage.getItem(key);
    return storedValue ? JSON.parse(storedValue) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

export default useLocalStorage;

كيفية استخدامه

import React from "react";
import useLocalStorage from "./useLocalStorage";

function ThemeSwitcher() {
  const [theme, setTheme] = useLocalStorage("theme", "light");

  return (
    <div>
      <p>الثيم الحالي: {theme}</p>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        تبديل الثيم
      </button>
    </div>
  );
}

export default ThemeSwitcher;

لماذا هذا رائع؟

لأنك نقلت المنطق بالكامل إلى Hook قابل لإعادة الاستخدام، بدل نسخه في كل مكوّن.


Custom Hook لجلب البيانات

import { useEffect, useState } from "react";

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  useEffect(() => {
    let isMounted = true;

    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) throw new Error("فشل الطلب");
        const result = await response.json();

        if (isMounted) {
          setData(result);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      isMounted = false;
    };
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

استخدامه

import React from "react";
import useFetch from "./useFetch";

function Posts() {
  const { data, loading, error } = useFetch(
    "https://jsonplaceholder.typicode.com/posts"
  );

  if (loading) return <p>جاري التحميل...</p>;
  if (error) return <p>{error}</p>;

  return (
    <div>
      {data?.slice(0, 5).map((post) => (
        <article key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </article>
      ))}
    </div>
  );
}

export default Posts;

useReducer: عندما تصبح الحالة أكثر تعقيدًا

أحيانًا useState يكون كافيًا جدًا.
لكن عندما تتعدد الأحداث التي تغيّر الحالة، قد يكون useReducer أنسب.

متى تفكر به؟

  • إذا كانت لديك عدة أنواع من التحديثات.

  • إذا كانت الحالة معقدة.

  • إذا أردت تنظيم منطق التغيير في مكان واحد.

مثال عدّاد باستخدام useReducer

import React, { useReducer } from "react";

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    case "RESET":
      return initialState;
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>العدد: {state.count}</p>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>زيادة</button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>نقصان</button>
      <button onClick={() => dispatch({ type: "RESET" })}>إعادة</button>
    </div>
  );
}

export default Counter;

لماذا useReducer جميل؟

لأن التغييرات تصبح واضحة، خاصة في التطبيقات التي تحتوي على منطق كثير.


useContext: مشاركة البيانات بين المكونات

عندما تحتاج تمرير بيانات بين طبقات كثيرة من المكوّنات، قد يصبح تمرير props متكررًا ومزعجًا.

هنا يأتي دور useContext.

مثال Theme Context

إنشاء السياق

import { createContext } from "react";

const ThemeContext = createContext();

export default ThemeContext;

المزود Provider

import React, { useState } from "react";
import ThemeContext from "./ThemeContext";

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  const toggleTheme = () => {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export default ThemeProvider;

استخدامه داخل مكوّن

import React, { useContext } from "react";
import ThemeContext from "./ThemeContext";

function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <header>
      <p>الوضع الحالي: {theme}</p>
      <button onClick={toggleTheme}>تبديل</button>
    </header>
  );
}

export default Header;

متى يكون useContext مناسبًا؟

  • إعدادات المستخدم.

  • الثيم.

  • اللغة.

  • معلومات المصادقة البسيطة.

  • البيانات المشتركة بين مكوّنات كثيرة.


مشروع صغير: قائمة مهام Todo App باستخدام Hooks

الآن لنربط أكثر من Hook في مثال عملي قريب من الواقع.

الفكرة

نريد تطبيق مهام يحتوي على:

  • إضافة مهمة.

  • حذف مهمة.

  • حفظ المهام في localStorage.

  • عدّاد للمهام المكتملة.

  • فلترة بسيطة.

الكود الكامل

import React, { useEffect, useMemo, useState } from "react";

function TodoApp() {
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem("todos");
    return saved ? JSON.parse(saved) : [];
  });

  const [text, setText] = useState("");
  const [filter, setFilter] = useState("all");

  useEffect(() => {
    localStorage.setItem("todos", JSON.stringify(todos));
  }, [todos]);

  const addTodo = () => {
    if (text.trim() === "") return;

    const newTodo = {
      id: Date.now(),
      title: text,
      completed: false,
    };

    setTodos((prev) => [...prev, newTodo]);
    setText("");
  };

  const toggleTodo = (id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const deleteTodo = (id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  };

  const filteredTodos = useMemo(() => {
    if (filter === "completed") {
      return todos.filter((todo) => todo.completed);
    }
    if (filter === "active") {
      return todos.filter((todo) => !todo.completed);
    }
    return todos;
  }, [todos, filter]);

  const completedCount = todos.filter((todo) => todo.completed).length;

  return (
    <div style={{ maxWidth: "600px", margin: "40px auto", fontFamily: "sans-serif" }}>
      <h1>تطبيق المهام</h1>

      <div>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="أضف مهمة جديدة"
        />
        <button onClick={addTodo}>إضافة</button>
      </div>

      <div style={{ marginTop: "20px" }}>
        <button onClick={() => setFilter("all")}>الكل</button>
        <button onClick={() => setFilter("active")}>غير مكتملة</button>
        <button onClick={() => setFilter("completed")}>مكتملة</button>
      </div>

      <p>المكتملة: {completedCount}</p>

      <ul>
        {filteredTodos.map((todo) => (
          <li key={todo.id} style={{ marginBottom: "10px" }}>
            <span
              onClick={() => toggleTodo(todo.id)}
              style={{
                textDecoration: todo.completed ? "line-through" : "none",
                cursor: "pointer",
                marginRight: "10px",
              }}
            >
              {todo.title}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>حذف</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;

ماذا استخدمنا هنا؟

  • useState لإدارة المهام والنص والفلتر.

  • useEffect للحفظ في localStorage.

  • useMemo لتصفية المهام بكفاءة.

  • وظائف منفصلة للتعامل مع الإضافة والحذف والتبديل.

هذا مثال ممتاز لكيف يمكن لـ Hooks أن تجعل التطبيق منظمًا ونظيفًا في الوقت نفسه.


أفضل ممارسات عند استخدام React Hooks

1) لا تضع منطقًا زائدًا داخل JSX

بدلًا من كتابة شروط كثيرة داخل return، حاول نقل المنطق إلى متغيرات أو دوال مساعدة.

2) استخدم أسماء واضحة

  • isLoading

  • isAuthenticated

  • userData

  • selectedTab

الأسماء الجيدة تختصر نصف الشرح.

3) لا تفرط في useMemo و useCallback

ليس كل شيء يحتاج إلى “تحسين الأداء”.
أحيانًا الكود الواضح أهم من الكود “المحسن” نظريًا.

4) افصل المنطق المتكرر إلى Custom Hooks

إذا وجدت نفسك تنسخ نفس useEffect في أكثر من مكان، فغالبًا هذا المرشح الأول لأن يتحول إلى Hook مخصص.

5) انتبه للاعتماديات dependency arrays

هذه نقطة حساسة جدًا.
نسيان اعتماد مهم قد يسبب سلوكًا غريبًا، وإضافة اعتماد غير ضروري قد تسبب renders إضافية.


أخطاء شائعة يقع فيها المبتدئون

الخطأ 1: تغيير state مباشرة

todos.push(newTodo);
setTodos(todos);

هذا خطأ لأنك تعدّل المصفوفة الأصلية مباشرة.

الصحيح

setTodos((prev) => [...prev, newTodo]);

الخطأ 2: نسيان إرجاع cleanup في effects

لو استخدمت مؤقتات أو subscriptions، تأكد من التنظيف.


الخطأ 3: عدم استخدام key صحيح في القوائم

{items.map((item, index) => (
  <li key={index}>{item.name}</li>
))}

استخدام index ليس دائمًا خطأ، لكنه غالبًا ليس الأفضل في القوائم التي يمكن أن تتغير.
إن كان لديك id فاستعمله.

{items.map((item) => (
  <li key={item.id}>{item.name}</li>
))}

الخطأ 4: وضع Hooks داخل الشروط

كما قلنا سابقًا، هذا يكسر قواعد Hooks.


الخطأ 5: تحديث state اعتمادًا على القيمة القديمة دون الشكل الوظيفي

إذا كانت القيمة الجديدة تعتمد على السابقة، استخدم:

setCount(prev => prev + 1);

كيف تفكر مثل مطور React Hooks؟

فكر بهذه الطريقة:

  • ما هي البيانات التي تتغير؟

  • ما الذي يجب أن يُعاد رسمه؟

  • ما الذي يحدث خارج واجهة المستخدم؟

  • ما المنطق الذي يتكرر؟

  • ما الذي يمكن عزله في Hook مخصص؟

عندما تبدأ بطرح هذه الأسئلة، ستلاحظ أن بناء مكوّنات React يصبح أشبه بتركيب قطع LEGO:
كل قطعة لها دور واضح.


مثال متقدم: Hook مخصص لتغيير عنوان الصفحة

import { useEffect } from "react";

function usePageTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]);
}

export default usePageTitle;

الاستخدام

import React from "react";
import usePageTitle from "./usePageTitle";

function About() {
  usePageTitle("صفحة من نحن");

  return <h1>من نحن</h1>;
}

export default About;

هذا مثال بسيط، لكنه يوضح قوة الـ Custom Hooks في ترتيب الكود.


مثال متقدم آخر: Hook لاكتشاف حجم النافذة

import { useEffect, useState } from "react";

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener("resize", handleResize);

    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return size;
}

export default useWindowSize;

استخدامه

import React from "react";
import useWindowSize from "./useWindowSize";

function ResponsiveInfo() {
  const { width, height } = useWindowSize();

  return (
    <p>
      العرض: {width}px، الارتفاع: {height}px
    </p>
  );
}

export default ResponsiveInfo;

Hooks ليست سحرًا، لكنها تعطيك نظامًا جميلًا

أحيانًا يشعر المتعلم أن Hooks شيء “معقد”.
لكن الحقيقة أنها طريقة لتقسيم المنطق بشكل منطقي جدًا.
كل Hook له مهمة:

  • useState للحالة.

  • useEffect للأثر الجانبي.

  • useRef للمرجع.

  • useMemo للحساب.

  • useCallback للدوال.

  • useContext للمشاركة.

  • useReducer للحالات الأكثر تنظيمًا.

بمجرد أن تربط كل أداة بمشكلتها، تصبح الصورة أوضح بكثير.


متى تستخدم كل Hook؟

استخدم useState عندما:

  • تكون الحالة بسيطة.

  • تحتاج قيمة أو قيمتين.

  • التحديث مباشر وواضح.

استخدم useEffect عندما:

  • تريد جلب بيانات.

  • تحتاج subscribe / cleanup.

  • لديك side effects.

استخدم useRef عندما:

  • تريد الوصول إلى DOM.

  • تحتاج قيمة ثابتة بين renders دون إعادة render.

استخدم useMemo عندما:

  • لديك عملية حسابية مكلفة.

  • هناك إعادة حساب غير ضرورية.

استخدم useCallback عندما:

  • تمرر دوال إلى مكوّنات children.

  • تريد تقليل إعادة إنشاء الدوال.

استخدم useContext عندما:

  • تكون البيانات مشتركة عبر عدة مستويات.

استخدم useReducer عندما:

  • تكون الحالة متعددة الخطوات أو معقدة.


كيف تتعلم Hooks بسرعة وبشكل فعلي؟

أفضل طريقة ليست الحفظ فقط، بل التطبيق.

جرّب هذه التمارين:

  1. عداد بسيط باستخدام useState.

  2. نموذج تسجيل دخول.

  3. جلب بيانات من API.

  4. مؤقت Stopwatch.

  5. قائمة مهام مع localStorage.

  6. تغيير الثيم باستخدام useContext.

  7. Hook مخصص لعرض حجم النافذة.

كلما كتبت أكثر، ستفهم أكثر.
React Hooks من تلك الأدوات التي لا تكفي معها القراءة وحدها. يجب أن تلمسها بيدك.


مشروع صغير تقترحه على نفسك بعد هذا المقال

جرّب بناء تطبيق بسيط فيه:

  • صفحة رئيسية.

  • قائمة مهام.

  • صفحة إعدادات.

  • ثيم داكن/فاتح.

  • حفظ البيانات في localStorage.

  • فلترة العناصر.

  • عدّاد للمهام المكتملة.

هذا المشروع الصغير سيجعلك تستخدم معظم Hooks الأساسية بشكل طبيعي جدًا.


أسئلة شائعة حول React Hooks

هل أستطيع استخدام Hooks في class components؟

لا، Hooks صُممت للمكوّنات الوظيفية.

هل يجب أن أستخدم كل Hook؟

لا، استخدم فقط ما تحتاجه.
الهدف ليس إدخال Hooks في كل مكان، بل اختيار الأداة المناسبة للمشكلة المناسبة.

هل useMemo و useCallback ضروريان دائمًا؟

لا. أحيانًا الكود بدونها أفضل وأوضح.

هل يمكن كتابة تطبيق كامل باستخدام Hooks فقط؟

نعم، وبشكل شائع جدًا اليوم.

هل Custom Hooks هي مكوّنات؟

لا، هي دوال تستخدم Hooks وتعيد منطقًا قابلًا لإعادة الاستخدام.


خاتمة

تعلم React Hooks يشبه تعلم لغة جديدة داخل React نفسها.
في البداية قد تحفظ الأسماء وتنسى متى تستخدم كل واحد، ثم تبدأ الأمور بالوضوح تدريجيًا، وبعدها تكتشف أنك لا تكتب React كما كنت تكتبها سابقًا.
تصبح الشفرة أكثر بساطة، وأكثر قابلية للفهم، وأقل ازدحامًا، وأقرب إلى طريقة التفكير الطبيعية في المشاكل.

لا تحاول أن تحفظ كل شيء دفعة واحدة.
ابدأ بـ useState و useEffect، ثم انتقل إلى useRef، وبعدها جرّب useMemo وuseCallback عندما تحتاجهما فعلًا، ثم استكشف useContext وuseReducer، وأخيرًا اصنع Hooks مخصصة خاصة بك.
هناك لحظة جميلة جدًا في تعلم Hooks: اللحظة التي تقول فيها في نفسك: “الآن فهمت لماذا كانت React تحب هذا الأسلوب”.
تلك اللحظة تستحق التجربة.

#React Hooks #تعلم React Hooks #React Hooks بالعربي #useState شرح #useEffect شرح #useRef React #useMemo React