شارك المقالة

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

في هذا المقال سنشرح أهم الأدوات المرتبطة بإدارة الحالة في React: useState, useMemo, useEffect, useCallback, useDeferredValue, useLayoutEffect, useTransition, useReducer, و useContext.

ما هي State في React؟

الـ State هي البيانات التي تتغير داخل المكون، وعند تغييرها يقوم React بإعادة رسم الواجهة بناءً على القيمة الجديدة.

المشكلة

إذا كان لديك رقم عداد أو نص داخل input وتريد تحديث الواجهة عند تغييره، فلا يمكنك الاعتماد على متغير عادي، لأن React لن يعرف أن هناك تغييرًا حدث.

الحل

نستخدم useState لحفظ القيمة وإخبار React بإعادة التحديث عند تغييرها.


import { useState } from "react";

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

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={() => setCount(count + 1)}>
        Increase
      </button>
    </div>
  );
}

export default Counter;
  

1. useState

يستخدم useState لإدارة الحالات البسيطة داخل المكون، مثل النصوص، الأرقام، القيم المنطقية، أو القوائم الصغيرة.

المشكلة

لديك input وتريد تخزين القيمة التي يكتبها المستخدم وعرضها مباشرة في الصفحة.

الحل

نستخدم useState لتخزين قيمة input وتحديثها عند الكتابة.


import { useState } from "react";

function UserName() {
  const [name, setName] = useState("");

  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={(event) => setName(event.target.value)}
        placeholder="Enter your name"
      />

      <p>Hello, {name}</p>
    </div>
  );
}

export default UserName;
  

2. useMemo

يستخدم useMemo لحفظ نتيجة عملية حسابية ثقيلة حتى لا يتم تنفيذها في كل مرة يتم فيها إعادة رسم المكون.

المشكلة

لديك قائمة منتجات وتريد حساب المنتجات الغالية، لكن العملية تتكرر مع كل تحديث حتى لو لم تتغير المنتجات.

الحل

نستخدم useMemo حتى يتم حساب النتيجة فقط عند تغير البيانات المطلوبة.


import { useMemo, useState } from "react";

function Products() {
  const [search, setSearch] = useState("");

  const products = [
    { id: 1, name: "Laptop", price: 1200 },
    { id: 2, name: "Mouse", price: 20 },
    { id: 3, name: "Phone", price: 900 },
  ];

  const expensiveProducts = useMemo(() => {
    console.log("Calculating expensive products...");

    return products.filter((product) => product.price > 500);
  }, [products]);

  return (
    <div>
      <input
        value={search}
        onChange={(event) => setSearch(event.target.value)}
        placeholder="Search..."
      />

      {expensiveProducts.map((product) => (
        <p key={product.id}>{product.name}</p>
      ))}
    </div>
  );
}

export default Products;
  

متى تستخدم useMemo؟

  • عند وجود عمليات حسابية ثقيلة.
  • عند فلترة أو ترتيب قوائم كبيرة.
  • عند منع إعادة تنفيذ كود لا يحتاج للتنفيذ في كل Render.

3. useEffect

يستخدم useEffect لتنفيذ كود بعد عملية Render، مثل جلب البيانات من API، التعامل مع localStorage، أو الاشتراك في أحداث معينة.

المشكلة

تريد جلب بيانات المستخدم من API عند فتح الصفحة.

الحل

نستخدم useEffect لتنفيذ عملية الجلب بعد تحميل المكون.


import { useEffect, useState } from "react";

function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    async function getUser() {
      const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
      const data = await response.json();

      setUser(data);
    }

    getUser();
  }, []);

  return (
    <div>
      {user ? <h2>{user.name}</h2> : <p>Loading...</p>}
    </div>
  );
}

export default UserProfile;
  

متى تستخدم useEffect؟

  • عند جلب البيانات من API.
  • عند تحديث عنوان الصفحة.
  • عند التعامل مع localStorage.
  • عند إضافة أو إزالة Event Listener.

4. useCallback

يستخدم useCallback لحفظ الدالة نفسها بين عمليات Render، حتى لا يتم إنشاء دالة جديدة في كل مرة.

المشكلة

عند تمرير دالة إلى مكون ابن، قد يتم إعادة رسم المكون الابن بدون داعي لأن الدالة يتم إنشاؤها من جديد مع كل Render.

الحل

نستخدم useCallback للحفاظ على نفس مرجع الدالة طالما أن الاعتماديات لم تتغير.


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

const Button = memo(function Button({ onClick }) {
  console.log("Button rendered");

  return <button onClick={onClick}>Increase</button>;
});

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

  const increaseCount = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <input
        value={name}
        onChange={(event) => setName(event.target.value)}
        placeholder="Type your name"
      />

      <h2>Count: {count}</h2>

      <Button onClick={increaseCount} />
    </div>
  );
}

export default Counter;
  

متى تستخدم useCallback؟

  • عند تمرير دوال إلى مكونات تستخدم memo.
  • عند وجود دالة داخل dependency array.
  • عند محاولة تقليل عمليات Render غير الضرورية.

5. useDeferredValue

يستخدم useDeferredValue لتأجيل تحديث قيمة معينة عندما يكون التحديث ثقيلًا على الواجهة.

المشكلة

لديك input يبحث داخل قائمة كبيرة، وعند الكتابة تصبح الواجهة بطيئة لأن React يحاول تحديث input ونتائج البحث الثقيلة في نفس الوقت.

الحل

نستخدم useDeferredValue لجعل قيمة البحث الثقيلة تتأخر قليلًا، بينما يبقى input سريعًا في الاستجابة.


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

function SearchList() {
  const [query, setQuery] = useState("");

  const deferredQuery = useDeferredValue(query);

  const items = Array.from({ length: 10000 }, (_, index) => {
    return `Item ${index + 1}`;
  });

  const filteredItems = useMemo(() => {
    return items.filter((item) =>
      item.toLowerCase().includes(deferredQuery.toLowerCase())
    );
  }, [deferredQuery]);

  return (
    <div>
      <input
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        placeholder="Search..."
      />

      <p>Searching for: {deferredQuery}</p>

      {filteredItems.slice(0, 20).map((item) => (
        <p key={item}>{item}</p>
      ))}
    </div>
  );
}

export default SearchList;
  

متى تستخدم useDeferredValue؟

  • عند البحث داخل قوائم كبيرة.
  • عند وجود تحديثات ثقيلة تؤثر على سرعة الكتابة.
  • عندما تريد إعطاء أولوية أعلى لتفاعل المستخدم.

6. useLayoutEffect

يشبه useLayoutEffect الـ useEffect، لكنه يعمل قبل أن يرسم المتصفح التغييرات على الشاشة.

المشكلة

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

الحل

نستخدم useLayoutEffect عندما نحتاج إلى قراءة أو تعديل layout قبل ظهور النتيجة للمستخدم.


import { useLayoutEffect, useRef, useState } from "react";

function BoxSize() {
  const boxRef = useRef(null);
  const [boxWidth, setBoxWidth] = useState(0);

  useLayoutEffect(() => {
    if (boxRef.current) {
      setBoxWidth(boxRef.current.offsetWidth);
    }
  }, []);

  return (
    <div>
      <div
        ref={boxRef}
        style={{
          width: "300px",
          padding: "20px",
          background: "#eee",
        }}
      >
        This is a box
      </div>

      <p>Box width: {boxWidth}px</p>
    </div>
  );
}

export default BoxSize;
  

متى تستخدم useLayoutEffect؟

  • عند قياس حجم عنصر قبل عرضه النهائي.
  • عند تعديل مكان عنصر بناءً على حجمه.
  • عند منع وميض بصري في الواجهة.

7. useTransition

يستخدم useTransition لتحديد أن تحديثًا معينًا ليس عاجلًا، وبالتالي يمكن لـ React تأجيله حتى تبقى الواجهة سريعة.

المشكلة

عند الكتابة في input وتحديث قائمة كبيرة في نفس الوقت، قد يشعر المستخدم أن الكتابة بطيئة.

الحل

نستخدم useTransition لجعل تحديث القائمة أقل أولوية من تحديث input.


import { useState, useTransition } from "react";

function SearchWithTransition() {
  const [inputValue, setInputValue] = useState("");
  const [list, setList] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleChange(event) {
    const value = event.target.value;

    setInputValue(value);

    startTransition(() => {
      const newList = Array.from({ length: 5000 }, (_, index) => {
        return `${value} - Result ${index + 1}`;
      });

      setList(newList);
    });
  }

  return (
    <div>
      <input
        value={inputValue}
        onChange={handleChange}
        placeholder="Search..."
      />

      {isPending && <p>Updating list...</p>}

      {list.slice(0, 20).map((item) => (
        <p key={item}>{item}</p>
      ))}
    </div>
  );
}

export default SearchWithTransition;
  

الفرق بين useTransition و useDeferredValue

useTransition يستخدم عندما تتحكم أنت في عملية تحديث State وتريد جعل هذا التحديث أقل أولوية.

أما useDeferredValue فيستخدم عندما لديك قيمة جاهزة وتريد نسخة مؤجلة منها.

8. useReducer

يستخدم useReducer عندما تصبح إدارة الحالة معقدة، أو عندما يكون لديك أكثر من عملية تغير نفس الحالة.

المشكلة

لديك عداد يحتوي على أكثر من Action مثل زيادة، تقليل، وتصفير. استخدام useState قد يصبح غير منظم إذا زاد المنطق.

الحل

نستخدم useReducer لوضع منطق تحديث الحالة في دالة واحدة واضحة.


import { useReducer } from "react";

const initialState = {
  count: 0,
};

function reducer(state, action) {
  switch (action.type) {
    case "increase":
      return { count: state.count + 1 };

    case "decrease":
      return { count: state.count - 1 };

    case "reset":
      return { count: 0 };

    default:
      return state;
  }
}

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

  return (
    <div>
      <h2>Count: {state.count}</h2>

      <button onClick={() => dispatch({ type: "increase" })}>
        Increase
      </button>

      <button onClick={() => dispatch({ type: "decrease" })}>
        Decrease
      </button>

      <button onClick={() => dispatch({ type: "reset" })}>
        Reset
      </button>
    </div>
  );
}

export default CounterReducer;
  

متى تستخدم useReducer؟

  • عندما تكون الحالة عبارة عن Object يحتوي على أكثر من قيمة.
  • عندما يوجد أكثر من Action لتحديث الحالة.
  • عندما يصبح استخدام useState غير منظم.

9. useContext

يستخدم useContext لمشاركة البيانات بين المكونات بدون تمريرها يدويًا عبر props من مكون إلى آخر.

المشكلة

لديك بيانات مثل theme أو بيانات المستخدم وتريد استخدامها في أكثر من مكون. تمريرها عبر props في كل مستوى يجعل الكود مزعجًا وصعب الصيانة.

الحل

نستخدم createContext و useContext لمشاركة البيانات مباشرة مع المكونات التي تحتاجها.


import { createContext, useContext, useState } from "react";

const ThemeContext = createContext(null);

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

  function toggleTheme() {
    setTheme((currentTheme) =>
      currentTheme === "light" ? "dark" : "light"
    );
  }

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

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

  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
}

function App() {
  return (
    <ThemeProvider>
      <ThemeButton />
    </ThemeProvider>
  );
}

export default App;
  

متى تستخدم useContext؟

  • عند مشاركة بيانات المستخدم بين عدة مكونات.
  • عند إدارة الثيم مثل dark mode و light mode.
  • عند مشاركة اللغة أو إعدادات التطبيق.

مقارنة سريعة بين الأنواع

Hookالاستخداممتى تحتاجه؟
useStateإدارة حالة بسيطةعداد، input، toggle
useMemoحفظ نتيجة عملية حسابيةفلترة، ترتيب، حسابات ثقيلة
useEffectتنفيذ كود بعد RenderAPI، localStorage، events
useCallbackحفظ مرجع الدالةتحسين الأداء مع memo
useDeferredValueتأجيل قيمة ثقيلةالبحث داخل قوائم كبيرة
useLayoutEffectتنفيذ كود قبل الرسم النهائيقياس العناصر ومنع الوميض
useTransitionجعل تحديث معين أقل أولويةالقوائم الثقيلة والتحديثات غير العاجلة
useReducerإدارة حالة معقدةActions متعددة ومنطق أكبر
useContextمشاركة الحالة بين المكوناتTheme، user، language

مثال عملي يجمع أكثر من Hook

في هذا المثال سنستخدم useState مع useMemo و useDeferredValue لبناء بحث سريع داخل قائمة منتجات.

المشكلة

البحث داخل قائمة كبيرة قد يسبب بطء في الكتابة لأن عملية الفلترة تحدث مع كل حرف.

الحل

نخزن النص باستخدام useState، ثم نؤجل قيمة البحث باستخدام useDeferredValue، وبعدها نحفظ نتيجة الفلترة باستخدام useMemo.


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

function ProductSearch() {
  const [search, setSearch] = useState("");

  const deferredSearch = useDeferredValue(search);

  const products = Array.from({ length: 10000 }, (_, index) => ({
    id: index + 1,
    name: `Product ${index + 1}`,
  }));

  const filteredProducts = useMemo(() => {
    return products.filter((product) =>
      product.name.toLowerCase().includes(deferredSearch.toLowerCase())
    );
  }, [deferredSearch]);

  return (
    <div>
      <h2>Products Search</h2>

      <input
        value={search}
        onChange={(event) => setSearch(event.target.value)}
        placeholder="Search product..."
      />

      <p>Results: {filteredProducts.length}</p>

      {filteredProducts.slice(0, 20).map((product) => (
        <p key={product.id}>{product.name}</p>
      ))}
    </div>
  );
}

export default ProductSearch;
  

أخطاء شائعة عند التعامل مع State في React

1. استخدام useMemo في كل مكان

لا تستخدم useMemo إلا عندما تكون العملية مكلفة فعلًا أو تؤثر على الأداء. استخدامه بدون سبب قد يجعل الكود أعقد بدون فائدة واضحة.

2. نسيان dependency array في useEffect

إذا نسيت dependency array، فقد يعمل useEffect مع كل Render، وهذا قد يسبب مشاكل في الأداء أو طلبات API متكررة.


// خطأ
useEffect(() => {
  fetchData();
});

// أفضل
useEffect(() => {
  fetchData();
}, []);
  

3. استخدام useContext لكل شيء

useContext مفيد لمشاركة البيانات، لكنه ليس بديلًا دائمًا لإدارة الحالة. إذا كانت الحالة تتغير كثيرًا أو كبيرة جدًا، قد تحتاج إلى تقسيم Context أو استخدام حل آخر لإدارة الحالة.

الخلاصة

إدارة الحالة في React لا تعتمد على useState فقط. كل Hook له استخدام مختلف ويحل مشكلة معينة.

استخدم useState للحالات البسيطة، و useReducer للحالات المعقدة، و useContext لمشاركة البيانات، و useMemo و useCallback لتحسين الأداء، و useEffect للتعامل مع التأثيرات الجانبية، أما useDeferredValue و useTransition فهما مفيدان جدًا عند التعامل مع تحديثات ثقيلة تحتاج إلى تجربة مستخدم أسرع.

Sources


لمزيد من المقالات : كيفية استخدام Hive في Flutter لإدارة البيانات بكفاءة
شاهد أيضًا
مقالات ذات صلة

🚫 مانع الإعلانات مفعل

يجب إيقاف مانع الإعلانات لاستكمال تصفح الموقع