ما الذي يجب أن يعرفه كل مطور React عن الحالة State؟

دقائق القراءة: 7

تُعد State من أهم المفاهيم التي يجب على كل مطور React فهمها بعمق، لأنها المسؤولة عن إدارة البيانات المتغيرة داخل الواجهة. وفهم آلية عملها لا يساعدك فقط على كتابة كود أنظف، بل يجنبك أيضاً كثيراً من الأخطاء التي تظهر أثناء بناء التطبيقات التفاعلية.

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

شرح أهم المفاهيم التي يجب أن يعرفها كل مطور ريأكت عن إدارة الحالة State

1. تحديثات useState لا يتم دمجها تلقائياً

من أكثر النقاط التي تُربك المطورين عند الانتقال من المكونات المبنية على الأصناف Class Components إلى المكونات الدالية Function Components أن تحديثات الحالة في useState لا تُدمج تلقائياً عندما تكون الحالة عبارة عن كائن Object.

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

import React from "react";

export default function App() {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");

  return (
    <form>
      <input
        name="email"
        type="email"
        placeholder="Email"
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        name="password"
        type="password"
        placeholder="Password"
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

لكن أحياناً نفضّل جمع بيانات النموذج داخل كائن واحد باستخدام useState لمرة واحدة فقط. هنا تظهر المشكلة الشائعة:

import React from "react";

export default function App() {
  const [state, setState] = React.useState({ email: '', password: '' });

  function handleInputChange(e) {
    setState({
      [e.target.name]: e.target.value
    });
  }

  return (
    <form>
      <input name="email" type="email" onChange={handleInputChange} />
      <input name="password" type="password" onChange={handleInputChange} />
      <button type="submit">Submit</button>
    </form>
  );
}

الخطأ هنا أن الاستدعاء السابق لـ setState يستبدل الكائن كاملاً، ولا يحتفظ تلقائياً بالقيم السابقة. أي أنك عند تعديل حقل واحد قد تفقد الحقول الأخرى.

مثال يوضح فقدان القيم السابقة عند تحديث useState دون دمج الكائن السابق

الحل الصحيح هو دمج الحالة السابقة يدوياً باستخدام معامل النشر spread operator:

import React from "react";

export default function App() {
  const [state, setState] = React.useState({ email: '', password: '' });

  function handleInputChange(e) {
    setState({
      ...state,
      [e.target.name]: e.target.value
    });
  }

  return (
    <form>
      <input name="email" type="email" onChange={handleInputChange} />
      <input name="password" type="password" onChange={handleInputChange} />
      <button type="submit">Submit</button>
    </form>
  );
}

الخلاصة هنا:

  • إذا كانت الحالة بسيطة، فغالباً الأفضل استخدام عدة متغيرات حالة منفصلة.
  • إذا استخدمت كائناً واحداً مع useState، فعليك دمج القيم السابقة بنفسك.
  • لا تتوقع من useState أن يتصرف مثل this.setState في المكونات الكلاسيكية.

2. تحديث الحالة يعيد التصيير، بينما useRef لا يفعل ذلك

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

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

المثال التالي يوضح ذلك:

export default function App() {
  const [skill, setSkill] = React.useState("");
  const [skills, setSkills] = React.useState(["HTML", "CSS", "JavaScript"]);

  function handleChangeInput(event) {
    setSkill(event.target.value);
  }

  function handleAddSkill() {
    setSkills(skills.concat(skill));
  }

  return (
    <>
      <input onChange={handleChangeInput} />
      <button onClick={handleAddSkill}>Add Skill</button>
      <SkillList skills={skills} />
    </>
  );
}

const SkillList = React.memo(({ skills }) => {
  console.log("rerendering");
  return (
    <ul>
      {skills.map((skill, i) => (
        <li key={i}>{skill}</li>
      ))}
    </ul>
  );
});

في هذا المثال، كل ضغطة على لوحة المفاتيح داخل الحقل تؤدي إلى تحديث skill، وبالتالي إعادة تصيير المكون الأب App. وبدون استخدام React.memo فإن المكون SkillList سيُعاد تصييره أيضاً، حتى لو لم تتغير بياناته فعلياً.

لهذا يساعد React.memo على تقليل التصيير غير الضروري للمكونات التي لا تتغير مدخلاتها.

ومن جهة أخرى، هناك أداة مهمة هي useRef. هذا الخطاف يسمح لك بتخزين قيمة داخل الخاصية .current دون أن يؤدي تحديثها إلى إعادة التصيير:

import React from "react";

export default function App() {
  const countRef = React.useRef(0);

  function handleAddOne() {
    countRef.current += 1;
  }

  return (
    <>
      <h1>Count: {countRef.current}</h1>
      <button onClick={handleAddOne}>+ 1</button>
    </>
  );
}

رغم أن القيمة تتغير فعلاً داخل countRef.current، فإن الواجهة لن تُحدّث تلقائياً، لأن useRef لا يطلق إعادة تصيير.

استخدم useState عندما تريد أن يظهر التغيير للمستخدم، واستخدم useRef عندما تحتاج إلى حفظ قيمة بين التصييرات دون التأثير على الواجهة.

3. يجب أن تكون تحديثات الحالة غير قابلة للتعديل المباشر Immutable

واحدة من أهم قواعد العمل مع React هي عدم تعديل الحالة مباشرة. يجب تحديث الحالة فقط عبر الدالة المخصصة التي يعيدها useState، مثل setCount أو setState.

إذا حاولت تغيير القيمة المخزنة في الحالة بأسلوب JavaScript مباشر، فلن يتعرف React على التغيير بالشكل الصحيح، وغالباً لن تحدث إعادة تصيير كما تتوقع.

import React from 'react';

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

  // Don't assign state to new (non-state) variables
  const newCount = count;

  // Don't directly mutate state
  const countPlusOne = count + 1;

  return (
    <>
      <h1>Count: {count}</h1>
    </>
  );
}

الفكرة الجوهرية هنا هي مبدأ immutability، أي أن بيانات الحالة يجب التعامل معها على أنها قيم جديدة عند التحديث، لا أن يتم العبث بالقيمة الأصلية ذاتها.

هذا المبدأ يمنحك عدة فوائد:

  • يسهّل على React اكتشاف التغييرات.
  • يجعل سلوك التطبيق أكثر قابلية للتوقع.
  • يقلل من الأخطاء المعقدة التي يصعب تتبعها.
  • يحسن من قابلية اختبار الكود وصيانته.

وسواء كنت تستخدم useState أو useReducer أو حتى مكتبات مثل Redux، فإن القاعدة نفسها تبقى ثابتة: لا تعدّل الحالة مباشرة، بل أنشئ قيمة جديدة ومررها عبر آلية التحديث الصحيحة.

4. تحديثات الحالة غير متزامنة ويتم جدولة تنفيذها

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

هذا السلوك مهم لتحسين الأداء، لأن React يحاول تنظيم التحديثات بدلاً من تنفيذ كل تغيير فوراً بطريقة قد تكون مكلفة.

لذلك من المهم أن تغيّر نموذجك الذهني:

  • setState لا يغيّر الحالة حالاً بالضرورة.
  • هو يطلب من React جدولة تحديث جديد.
  • موعد تطبيق التحديث الفعلي تحدده آلية التصيير الداخلية.

وهنا يظهر فرق مهم بين useState وuseRef:

  • تحديث useState مجدول وقد لا يكون فورياً.
  • تحديث useRef على .current يحدث مباشرة من حيث القيمة، لكنه لا يعيد التصيير.

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

5. الحالة القديمة Stale State قد تظهر بسبب closures

من أكثر المشكلات الدقيقة في React ما يُعرف باسم stale state، أي الاعتماد على نسخة قديمة من الحالة أثناء التحديث. ويحدث ذلك غالباً داخل closure، أي عندما تستخدم دالة قيمة من نطاق خارجي وتحتفظ بها لوقت لاحق.

يتضح هذا في المثال التالي الذي يستخدم setTimeout:

import React from 'react';

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

  function delayAddOne() {
    setTimeout(() => {
      setCount(count + 1);
    }, 1000);
  }

  return (
    <>
      <h1>Count: {count}</h1>
      <button onClick={delayAddOne}>+ 1</button>
    </>
  );
}

المشكلة هنا أن الدالة داخل setTimeout تلتقط قيمة count القديمة وقت إنشائها، وليس بالضرورة أحدث قيمة بعد النقرات المتعددة.

توضيح مشكلة stale state في React عند استخدام setTimeout مع العداد

الحل الأفضل هو استخدام التحديث الدالي functional update، بحيث تمرر دالة إلى setCount تستقبل القيمة السابقة الحقيقية ثم تعيد القيمة الجديدة:

import React from 'react';

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

  function delayAddOne() {
    setTimeout(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);
  }

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={delayAddOne}>+ 1</button>
    </div>
  );
}

بهذه الطريقة لن تعتمد على القيمة القديمة داخل closure، بل على آخر قيمة موثوقة تديرها React لحظة تنفيذ التحديث.

استخدم هذا الأسلوب خصوصاً في الحالات التالية:

  • عند التحديث اعتماداً على القيمة السابقة.
  • عند وجود setTimeout أو setInterval.
  • عند التعامل مع أحداث متتابعة وسريعة.
  • عند تنفيذ أكثر من تحديث متقارب على نفس الحالة.

أفضل ممارسات عملية لإدارة State في React

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

الخلاصة التقنية

فهم State في React لا يقتصر على معرفة طريقة استخدام useState فقط، بل يشمل إدراك طبيعة التحديثات، وعلاقتها بإعادة التصيير، وخطورة التعديل المباشر، واحتمالات الوقوع في مشكلة stale state. من الناحية التقنية، المطور المحترف هو من يختار الأداة المناسبة بين useState وuseRef ويدير التحديثات بطريقة غير مباشرة وآمنة وقابلة للتوسع. وكلما كان فهمك لهذه التفاصيل أعمق، أصبحت تطبيقاتك أكثر استقراراً وأفضل أداءً وأسهل صيانة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *