EduFlow
أساسيات React
⁦0%⁩

الوحدة 3 · الـ State والأحداث

أساسيات useEffect

19 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

استخدم useEffect لإبقاء الـ state في React متزامنًا مع أنظمة خارجها، واكتب الاعتماديات ودوال التنظيف بشكل صحيح، وتعرّف على الحالات الكثيرة التي لا تحتاج فيها إلى effect أصلًا.

  1. 1أول effect لك
  2. 2التنظيف
  3. 3حفظ Watchlist في localStorage
  4. 4قد لا تحتاج إلى effect

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

حتى الآن، كان كل ما في الـ components متعلّقًا بـ React: props تدخل، وJSX يخرج، وتغييرات الـ state تُطلق الـ render. لكن Watchlist يحتاج أيضًا إلى لمس أشياء لا تديرها React. يجب أن يقول تبويب المتصفّح «2 to watch»، ويجب أن تنجو القائمة من تحديث الصفحة بحفظها في localStorage. لا شيء من هذا رسم. إنه تزامن (synchronization): إبقاء شيء خارج React متوافقًا مع الـ state الخاص بك. وهذه وظيفة الـ effects.

أول effect لك

src/App.jsx
import { useEffect, useState } from 'react';

export default function App() {
  const [movies, setMovies] = useState(INITIAL_MOVIES);
  const left = movies.filter((m) => !m.watched).length;

  useEffect(() => {
    document.title = `${left} to watch · Watchlist`;
  }, [left]);

  // …render the app
}

تأخذ useEffect دالة ومصفوفة اعتماديات (dependency array). ترسم React الـ component، وتحدّث الشاشة، ثم تشغّل الدالة. وبعد مرّات الـ render اللاحقة، لا تشغّلها مجددًا إلا إذا تغيّر شيء في مصفوفة الاعتماديات منذ المرة السابقة. علّم فيلمًا كمُشاهَد، فتنتقل left من 2 إلى 1، ويتحدّث العنوان. اكتب في نموذج الإضافة، فلا تتغيّر left، ويُتخطّى الـ effect.

لمصفوفة الاعتماديات ثلاثة أشكال:

ما تكتبه متى يعمل الـ effect
useEffect(fn, [a, b]) بعد الـ render الأول، وكلما تغيّرت a أو b
useEffect(fn, []) بعد الـ render الأول فقط
useEffect(fn) بعد كل render

وقاعدة ما يوضع في المصفوفة ليست خيارًا: اسرد كل قيمة من الـ component يقرؤها الـ effect، مثل الـ props والـ state والمتغيّرات المشتقّة منهما. وقاعدة الـ lint الخاصة بالاعتماديات في مشروع Vite (react/exhaustive-deps في Oxlint، وreact-hooks/exhaustive-deps في ESLint) تتحقّق من ذلك نيابةً عنك. إذا أغفلت شيئًا، فسيظل الـ effect يستخدم قيمة قديمة، وهذا هو الخطأ في سؤال الاختبار الثالث.

التنظيف

بعض الـ effects تبدأ شيئًا يجب إيقافه: مؤقّتًا، أو مستمع أحداث، أو اتصالًا. أرجِع دالة تلغيه:

JSX
useEffect(() => {
  function handleKeyDown(e) {
    if (e.key === '/') focusSearch();
  }
  window.addEventListener('keydown', handleKeyDown);
  return () => window.removeEventListener('keydown', handleKeyDown);
}, []);

تستدعي React دالة التنظيف قبل تشغيل الـ effect مجددًا، وحين يُزال الـ component من الشاشة. ومن دونها، سيضيف كل تركيب مستمعًا آخر، وسيعمل الضغط على «/» عدّة مرات.

بعد الـ render الأول والـ commit، يعمل الـ effect. وحين تتغيّر إحدى الاعتماديات، تعيد React الـ render، وتشغّل دالة التنظيف السابقة، ثم تشغّل الـ effect مجددًا. وحين يُلغى تركيب الـ component، تشغّل React دالة التنظيف مرة أخيرة. Render + commit Effect تغيّرت الاعتماديات: re-render التنظيف، ثم الـ Effect مجددًا Unmount: تنظيف يتكرّر مع كل تغيير
دورة حياة الـ effect

لماذا يعمل مرتين في وضع التطوير

مع <StrictMode> (الموجود في main.jsx لديك)، تفعل React شيئًا غريبًا أثناء التطوير: تركّب الـ component، وتشغّل الـ effects، ثم تشغّل دوال تنظيفها فورًا كأنها تلغي التركيب، ثم تشغّل الـ effects مجددًا. إنه اختبار ضغط. إذا كانت دالة التنظيف تلغي الـ effect بشكل صحيح، فسيتصرّف التطبيق بالطريقة نفسها، ولن ترى إلا سطرًا إضافيًا في السجل. وإن لم تكن كذلك، كأن تنسى إزالة مستمع، فستلاحظ الآن بدل أن تلاحظ في الإنتاج. لا تحاول منع التشغيل المزدوج؛ بل أصلح دالة التنظيف.

حفظ Watchlist في localStorage

حفظ الـ state هو الـ effect النموذجي: كلما تغيّرت movies، اكتبها في التخزين.

src/App.jsx
const STORAGE_KEY = 'watchlist.movies';

function loadMovies() {
  try {
    return JSON.parse(localStorage.getItem(STORAGE_KEY)) ?? [];
  } catch {
    return [];
  }
}

export default function App() {
  const [movies, setMovies] = useState(loadMovies);

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

  // …
}

هناك تفصيلتان مهمّتان. التحميل يحدث في دالة التهيئة (initializer)، useState(loadMovies)، لا في effect. تمرير الدالة (لا استدعاؤها) يعني أن React تقرأ التخزين للـ render الأول فقط، لا في كل render، فتظهر القائمة المحفوظة فورًا. أمّا التحميل في effect فسيرسم قائمة فارغة أولًا، وقد يكتب effect الحفظ تلك المصفوفة الفارغة فوق أفلامك المخزّنة. كما تستخدم loadMovies كتلة try/catch، لأن التخزين قد يكون محظورًا أو يحتوي على بيانات تالفة، والقيمة المعطوبة يجب ألّا تُسقط التطبيق كله.

قد لا تحتاج إلى effect

الـ effects مخرج طوارئ، وأكثر أخطائها شيوعًا تأتي من استخدامها حيث لا حاجة إليها. قبل أن تكتب واحدًا، افحص هذه الحالات الثلاث.

حساب شيء من الـ props أو الـ state. القوائم المصفّاة والأعداد والنصوص المنسّقة تُحسب أثناء الـ render. تخزينها في الـ state ومزامنتها بـ effect يضيف render إضافيًا ببيانات قديمة، ومزيدًا من الـ state الذي قد ينحرف.

الاستجابة لإجراء من المستخدم. إذا كان يجب أن يُنفَّذ الكود لأن المستخدم نقر Add، فضعه في معالج الإرسال. الـ effect الذي يراقب movies لا يستطيع التمييز بين الإضافة والحذف وإعادة التحميل.

إعادة ضبط الـ state حين يتغيّر prop. بدل effect يمسح مسوّدة حين تتغيّر movieId، امنح الـ component قيمة key: <NotesEditor key={movieId} />. الـ key الجديد يعني component جديدًا بـ state جديد.

وما يتبقّى هو التزامن الحقيقي: واجهات المتصفّح، والتخزين، والمؤقّتات، والاشتراكات، واتصالات الشبكة. قائمة قصيرة، وهي القائمة الصحيحة.

بهذا يكتمل جوهر React: الـ components والـ props والـ state والأحداث والـ effects. في القسم التالي تجمع كل القطع في Watchlist المكتمل، وتنسّقه، وتنشره.

أهم النقاط

  • يعمل الـ effect بعد أن تحدّث React الشاشة، ووظيفته التزامن مع شيء خارج React، مثل عنوان المستند أو localStorage أو المؤقّتات أو الاشتراكات.
  • تسرد مصفوفة الاعتماديات كل قيمة من الـ component يقرؤها الـ effect؛ وتعيد React تشغيل الـ effect حين تتغيّر إحداها.
  • أرجِع دالة تنظيف (cleanup) تلغي ما فعله الـ effect؛ تشغّلها React قبل التشغيل التالي وعند إلغاء التركيب.
  • في وضع التطوير، يشغّل StrictMode كل effect، ثم دالة تنظيفه، ثم الـ effect من جديد، ليثبت أن التنظيف يعمل.
  • إذا كان يمكن حساب القيمة من الـ props أو الـ state، أو كان التغيير ناتجًا عن إجراء محدّد من المستخدم، فلا تحتاج إلى effect.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

تُحفظ visibleMovies في الـ state وتُحدَّث بـ useEffect(() => setVisibleMovies(movies.filter(…)), [movies, filter]). ما النهج الأفضل؟

السؤال 2 من 4

في وضع التطوير، effect يطبع «subscribed» وله دالة تنظيف تطبع «unsubscribed»، يطبع عند تحميل الصفحة «subscribed, unsubscribed, subscribed». ما الذي يحدث؟

السؤال 3 من 4

ما الخطأ في هذا الـ effect؟

JSX
useEffect(() => {
  document.title = `${left} to watch`;
}, []);
السؤال 4 من 4

بعد إضافة فيلم تريد عرض رسالة «Movie added». أين يجب أن يوضع الكود الذي يعرضها؟

تدريب

بيئة تجريب الكود

زامن عنوان التبويب

مهمّتان في Watchlist:

  1. احذف الـ effect غير الضروري. تُنسخ left إلى الـ state بواسطة effect. احسبها أثناء الـ render بدلًا من ذلك.
  2. زامن عنوان تبويب المتصفّح. ما دام Watchlist على الشاشة، يجب أن تكون قيمة document.title هي 2 to watch · Watchlist (بالرقم الحقيقي)، وأن تتحدّث حين يُعلَّم فيلم كمُشاهَد. وحين يُزال Watchlist (انقر Hide watchlist)، يجب أن يعود العنوان إلى ما كان عليه قبل ذلك، أي Playground.
import { useEffect, useState } from 'react';

// The page's own title, before the Watchlist changes it.
document.title = 'Playground';

const INITIAL = [
  { id: 'a1', title: 'Arrival', watched: true },
  { id: 'p2', title: 'Past Lives', watched: false },
  { id: 'd3', title: 'Dune: Part Two', watched: false },
];

function Watchlist() {
  const [movies, setMovies] = useState(INITIAL);
  const [left, setLeft] = useState(0);

  // 1. This Effect only copies derived data into state. Replace it.
  useEffect(() => {
    setLeft(movies.filter((m) => !m.watched).length);
  }, [movies]);

  // 2. Sync document.title, and restore the old title on cleanup.

  function toggle(id) {
    setMovies(movies.map((m) => (m.id === id ? { ...m, watched: !m.watched } : m)));
  }

  return (
    <section>
      <p id="left">{left} to watch</p>
      <ul>
        {movies.map((m) => (
          <li key={m.id}>
            {m.title}
            <button onClick={() => toggle(m.id)}>{m.watched ? 'Watched' : 'Mark watched'}</button>
          </li>
        ))}
      </ul>
    </section>
  );
}

export default function App() {
  const [show, setShow] = useState(true);
  return (
    <main>
      <button id="hide" onClick={() => setShow(false)}>Hide watchlist</button>
      {show && <Watchlist />}
    </main>
  );
}
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

المعاينة

الـ console

(لا مخرجات)

الفحوص

  • تعرض الصفحة “2 to watch”
  • عنوان التبويب هو “2 to watch · Watchlist”
  • تعليم Past Lives كمُشاهَد يحدّث العنوان إلى “1 to watch · Watchlist”
  • إخفاء Watchlist يعيد العنوان “Playground”