useMemo

ما هي useMemo؟

هو هوك يرجّع لك قيمة مُحاسَبة (computed value) مُخزّنة تُعاد حسابها فقط عندما تتغيّر الاعتمادات (dependencies). فكر فيها كـ “كاش” لنتيجة دالة غالية التكلفة.

التوقيع:

const memoizedValue = useMemo(() => computeSomething(a, b), [a, b]);
JSX
  • الدالة داخل useMemo تُنفَّذ فقط إذا تغيّر أحد عناصر المصفوفة [a, b].
  • مفيدة لتجنّب الحسابات الثقيلة كل رندر، أو للمحافظة على المساواة المرجعية (Referential Equality) لقيم/كائنات نمررها كمزايا (props) أو نعتمد عليها في useEffect.
متى أستخدمها؟
  • عند وجود حساب ثقيل (فرز/تجميع/فلترة ضخمة، صياغة بيانات رسومات…).
  • عندما تحتاج ثبات المرجع لكائن/مصفوفة مُمَرّرة لـ child memoized لتقليل rerenders.
  • لا تستخدمها لكل شيء؛ لو الحساب رخيص، اتركها بسيطة بلا useMemo.
أمثلة أساسية
1) حساب مكلف (Expensive Computation)
import { useMemo, useState } from "react";

function heavySum(n) {
  // محاكاة عملية ثقيلة
  let s = 0;
  for (let i = 0; i < 5_000_000; i++) s += (i % 10);
  return s + n;
}

export default function ExpensiveExample() {
  const [n, setN] = useState(0);
  const [text, setText] = useState("");

  const result = useMemo(() => heavySum(n), [n]); 
  // ستحسب فقط عند تغيّر n، الكتابة في input الثاني لن تعيد الحساب

  return (
    <div>
      <input type="number" value={n} onChange={(e) => setN(+e.target.value)} />
      <input value={text} onChange={(e) => setText(e.target.value)} placeholder="لا يؤثر على الحساب" />
      <p>النتيجة: {result}</p>
    </div>
  );
}
JSX

2) فلترة وفرز قائمة كبيرة

import { useMemo, useState } from "react";

export default function FilterSortList({ items }) {
  const [q, setQ] = useState("");
  const [sortAsc, setSortAsc] = useState(true);

  const visible = useMemo(() => {
    const filtered = items.filter(it =>
      it.name.toLowerCase().includes(q.toLowerCase())
    );
    return filtered.sort((a, b) =>
      sortAsc ? a.price - b.price : b.price - a.price
    );
  }, [items, q, sortAsc]);

  return (
    <div>
      <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="ابحث..." />
      <button onClick={() => setSortAsc(s => !s)}>تبديل الترتيب</button>
      <ul>{visible.map(it => <li key={it.id}>{it.name} - ${it.price}</li>)}</ul>
    </div>
  );
}
JSX

3) تثبيت مرجع كائن/مصفوفة تمرّرها لطفل (لمنع rerenders)

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

const Child = memo(function Child({ options }) {
  // سيُعاد رندر Child فقط عندما يتغيّر محتوى options فعلاً
  console.log("Child render");
  return <pre>{JSON.stringify(options)}</pre>;
});

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

  // بدون useMemo: كل رندر ينشئ كائنًا جديدًا ⇒ Child يعيد الرندر
  const options = useMemo(
    () => ({ page: 1, pageSize: 20, show: count % 2 === 0 }),
    [count] // يتغير عند تغيّر count (فعلاً المحتوى يتغير)
  );

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <Child options={options} />
    </div>
  );
}
JSX

4) اشتقاق حالة من مدخلات متعددة (Derived Data)

import { useMemo, useState } from "react";

export default function CartTotals({ items }) {
  const [taxRate, setTaxRate] = useState(0.15);

  const { subtotal, tax, total } = useMemo(() => {
    const subtotal = items.reduce((s, it) => s + it.price * it.qty, 0);
    const tax = subtotal * taxRate;
    return { subtotal, tax, total: subtotal + tax };
  }, [items, taxRate]);

  return (
    <div>
      <p>Subtotal: {subtotal.toFixed(2)}</p>
      <p>Tax: {tax.toFixed(2)}</p>
      <p>Total: {total.toFixed(2)}</p>
      <button onClick={() => setTaxRate(r => (r === 0.15 ? 0.2 : 0.15))}>
        Toggle Tax
      </button>
    </div>
  );
}
JSX

useMemo vs useCallback vs memo

  • useMemo(fn, deps): يخزّن قيمة ناتجة عن دالة.
  • useCallback(fn, deps): يخزّن دالة نفسها (مرجع ثابت)؛ مكافئ لـ useMemo(() => fn, deps).
  • memo(Component): يغلف كومبوننت ليمنع رندرة إلا إذا تغيّرت props بقيمة مختلفة (shallow compare).
    غالبًا نستخدم useMemo/useCallback مع memo لتثبيت مراجع props.
ملاحظات وأفضل ممارسات
  • لا تبالغ: useMemo له كلفة (مقارنة deps وتخزين). استخدمه عندما:
    1. الحساب فعلاً مكلف، أو
    2. تحتاج ثبات مرجع لكائن/مصفوفة/دالة لخفض rerenders.
  • اجعل قائمة الاعتمادات دقيقة؛ لو نسيت اعتماد، قد تعرض قيمة قديمة.
  • حافظ على الدالة داخل useMemo خالية من الآثار الجانبية (Pure). لا تعمل setState داخلها.
  • لو كنت تحتاج قراءة/جلب بيانات → هذا دور useEffect وليس useMemo.
أخطاء شائعة
  • ❌ وضع دالة fetch داخل useMemo بدل useEffect.
  • ❌ استخدام useMemo حول أشياء رخيصة (لن تربح شيئًا).
  • ❌ نسيان deps أو تمرير كائنات غير ثابتة في deps (فتُحسب كل مرة).

لمحة TypeScript (اختياري)

const list = useMemo<string[]>(() => data.map(d => d.name), [data]);
JSX
مثال صغير يجمع useMemo مع memo وuseCallback
import { memo, useCallback, useMemo, useState } from "react";

const List = memo(function List({ items, onPick }) {
  console.log("List render");
  return (
    <ul>
      {items.map(it => (
        <li key={it.id} onClick={() => onPick(it.id)}>
          {it.label}
        </li>
      ))}
    </ul>
  );
});

export default function Combo() {
  const [q, setQ] = useState("");
  const [picked, setPicked] = useState(null);

  const raw = useMemo(
    () => Array.from({ length: 1000 }, (_, i) => ({ id: i, label: `Item ${i}` })),
    [] // يُنشأ مرة واحدة
  );

  const filtered = useMemo(() => {
    if (!q) return raw;
    return raw.filter(x => x.label.toLowerCase().includes(q.toLowerCase()));
  }, [raw, q]); // فلترة مكلفة تُحسب فقط عند الحاجة

  const handlePick = useCallback((id) => setPicked(id), []); // مرجع دالة ثابت

  return (
    <div>
      <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="فلترة..." />
      <List items={filtered} onPick={handlePick} />
      <p>المختار: {picked ?? "لا شيء"}</p>
    </div>
  );
}
JSX