كيف تعمل الحالة في React؟ شرح عملي مع أمثلة برمجية واضحة

دقائق القراءة: 10
شرح مفهوم الحالة في React مع أمثلة برمجية توضح تحديث واجهة المستخدم

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

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

كيف تعرض React البيانات داخل واجهة المستخدم؟

لكي تعرض React أي عنصر على الشاشة، فإنها تعتمد على الدالة ReactDOM.render(). هذه الدالة تستقبل عنصراً أو مكوناً، ثم تُدخله داخل عنصر موجود مسبقاً في الصفحة.

ReactDOM.render(element, container[, callback])
  • element: قد يكون عنصراً من HTML أو شيفرة JSX أو مكوناً يعيد JSX.
  • container: هو العنصر الذي سيتم حقن المحتوى داخله في الواجهة.
  • callback: دالة اختيارية تُنفَّذ بعد اكتمال العرض أو إعادة العرض.

مثال أول: عرض عنصر واحد

import React from "react";
import ReactDOM from "react-dom";

const rootElement = document.getElementById("root");

ReactDOM.render(<h1>Welcome to React!</h1>, rootElement);

في هذا المثال، تعرض React عنواناً واحداً فقط داخل العنصر root.

مثال ثانٍ: عرض عدة عناصر معاً

import React from "react";
import ReactDOM from "react-dom";

const rootElement = document.getElementById("root");

ReactDOM.render(
  <div>
    <h1>Welcome to React!</h1>
    <p>React is awesome.</p>
  </div>,
  rootElement
);

عندما نحتاج إلى أكثر من عنصر، نُغلفها داخل عنصر أب مثل <div>.

تخزين JSX في متغير لسهولة التنظيم

import React from "react";
import ReactDOM from "react-dom";

const rootElement = document.getElementById("root");

const content = (
  <div>
    <h1>Welcome to React!</h1>
    <p>React is awesome.</p>
  </div>
);

ReactDOM.render(content, rootElement);

هذا الأسلوب أفضل حين يصبح المحتوى أطول، لأنه يجعل الشيفرة أوضح وأسهل في القراءة والصيانة.

لماذا لا تتحدث الواجهة عند تغيير المتغيرات العادية؟

لنأخذ مثالاً بسيطاً على عدّاد:

import React from "react";
import ReactDOM from "react-dom";

const rootElement = document.getElementById("root");
let counter = 0;

const handleClick = () => {
  counter++;
  console.log("counter", counter);
};

const content = (
  <div>
    <button onClick={handleClick}>Increment counter</button>
    <div>Counter value is {counter}</div>
  </div>
);

ReactDOM.render(content, rootElement);

مثال يوضح زيادة قيمة العداد في console دون تحديث واجهة React

على الرغم من أن قيمة counter تتغير فعلاً عند الضغط على الزر، فإن الواجهة لا تتحدث. السبب هو أن ReactDOM.render() تم استدعاؤها مرة واحدة فقط عند تحميل الصفحة، لذلك لم تُطلب من React إعادة عرض القيم الجديدة.

إعادة العرض يدوياً

import React from "react";
import ReactDOM from "react-dom";

const rootElement = document.getElementById("root");
let counter = 0;

const handleClick = () => {
  counter++;
  console.log("counter", counter);
  renderContent();
};

const renderContent = () => {
  const content = (
    <div>
      <button onClick={handleClick}>Increment counter</button>
      <div>Counter value is {counter}</div>
    </div>
  );

  ReactDOM.render(content, rootElement);
};

renderContent();

هنا أنشأنا دالة اسمها renderContent() وطلبنا منها إعادة العرض بعد كل ضغطة. هذه الطريقة تنجح من الناحية التعليمية، لكنها ليست الأسلوب العملي الذي نعتمد عليه في تطبيقات React الحديثة.

تحديث واجهة React بعد إعادة الاستدعاء اليدوي لعملية render

هل إعادة العرض مكلفة دائماً؟

قد يبدو أن إعادة العرض المتكرر تعني إعادة بناء كامل DOM في كل مرة، لكن React تستخدم آلية Virtual DOM لتحديد ما تغيّر فعلياً، ثم تحدّث العناصر الضرورية فقط. لذلك فهي أكثر كفاءة مما قد يبدو لأول وهلة.

توضيح بصري لكيفية تحديث React للعناصر المتغيرة فقط عبر Virtual DOM

مع ذلك، لا يُعد استدعاء دالة إعادة العرض يدوياً عند كل تحديث أسلوباً مناسباً. لهذا السبب ظهر مفهوم State.

ما هي الحالة State في React؟

State هي كائن يحتوي على البيانات المتغيرة التي يحتاج المكون إلى تتبعها والتفاعل معها. كلما تغيّرت هذه البيانات بالطريقة الصحيحة، تُعيد React عرض المكون تلقائياً.

في React، نكتب الشيفرة داخل مكونات، وهناك طريقتان شهيرتان لإنشاء المكونات:

  • المكونات الصنفية Class Components
  • المكونات الدالية Functional Components

سنبدأ بالمكونات الصنفية لفهم الأساس، ثم ننتقل لاحقاً إلى المكونات الدالية وHooks.

استخدام State داخل المكونات الصنفية

import React from "react";
import ReactDOM from "react-dom";

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.state.counter = this.state.counter + 1;
    console.log("counter", this.state.counter);
  }

  render() {
    const { counter } = this.state;

    return (
      <div>
        <button onClick={this.handleClick}>Increment counter</button>
        <div>Counter value is {counter}</div>
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

اسم المكون هنا يبدأ بحرف كبير مثل Counter، وهذا مهم جداً لأن React تميّز بهذه الطريقة بين المكونات والعناصر العادية.

في constructor قمنا بالآتي:

  • استدعاء super(props).
  • تهيئة this.state بقيمة أولية.
  • ربط this مع الدالة handleClick باستخدام bind().

توضيح أثر التعديل المباشر على state دون إعادة تحديث الواجهة في React

المشكلة هنا أننا عدّلنا State مباشرة بهذه الصيغة:

this.state.counter = this.state.counter + 1;

وهذا خطأ شائع. لا يجب أبداً تعديل State مباشرة، لأن React لن تعتبر ذلك تحديثاً رسمياً يستحق إعادة العرض، كما أن هذا الأسلوب قد يسبب سلوكاً غير متوقع داخل التطبيق.

الصيغة الصحيحة: استخدام setState()

لتحديث الحالة بالشكل الصحيح، توفّر React الدالة setState():

setState(updater, [callback])
  • updater: يمكن أن يكون كائناً أو دالة.
  • callback: دالة اختيارية تُنفّذ بعد اكتمال تحديث الحالة.

بمجرد استدعاء setState()، تقوم React بإعادة عرض المكون وأبنائه عند الحاجة، من دون الحاجة إلى استدعاء render يدوياً.

التحديث بالاعتماد على الحالة السابقة

handleClick() {
  this.setState((prevState) => {
    return { counter: prevState.counter + 1 };
  });
  console.log("counter", this.state.counter);
}

هنا استخدمنا دالة تمرّر لنا prevState، ثم أعدنا كائناً جديداً يحتوي على القيمة المحدّثة. هذا الأسلوب هو الأفضل عندما تعتمد القيمة الجديدة على القيمة القديمة.

عرض تحديث قيمة العداد في الواجهة مع تأخر ظهور القيمة الجديدة في console بسبب async setState

ستلاحظ أن الواجهة تتحدث بشكل صحيح، لكن console.log() قد يطبع القيمة القديمة. السبب أن setState() تعمل بشكل غير متزامن asynchronous، أي إن التحديث لا يحدث فوراً في نفس السطر.

كيف تحصل على القيمة الجديدة بعد التحديث؟

handleClick() {
  this.setState(
    (prevState) => {
      return { counter: prevState.counter + 1 };
    },
    () => console.log("counter", this.state.counter)
  );
}

تنفيذ callback بعد setState للحصول على القيمة المحدثة مباشرة في React

يمكن تمرير دالة ثانية إلى setState() تُنفّذ بعد اكتمال التحديث. هذه الطريقة مناسبة للاختبار السريع أو تسجيل القيم، لكن في التطبيقات العملية يُفضّل غالباً استخدام دورة الحياة المناسبة.

استخدام componentDidUpdate() بدلاً من callback

componentDidUpdate(prevProps, prevState) {
  if (prevState.counter !== this.state.counter) {
    console.log("counter", this.state.counter);
  }
}

تُعد componentDidUpdate() أكثر ملاءمة عندما تريد تنفيذ منطق محدد بعد تغيّر الحالة أو props.

تبسيط كتابة المكونات الصنفية

الصيغة التقليدية باستخدام constructor وbind() قد تكون طويلة، لذلك يمكن استخدام خصائص الأصناف class properties لتقليل التعقيد.

state = { counter: 0 };

handleClick = () => {
  this.setState((prevState) => {
    return { counter: prevState.counter + 1 };
  });
};

عند استخدام الدوال السهمية arrow functions لا نحتاج إلى bind()، لأن this تُفهم تلقائياً من سياق الصنف.

اختصار الإرجاع في دوال الأسهم

this.setState((prevState) => ({ counter: prevState.counter + 1 }));

بما أن الدالة تعيد كائناً واحداً فقط، يمكن اختصارها إلى سطر واحد. لاحظ أننا وضعنا الكائن بين قوسين دائريين حتى تميّزه JavaScript على أنه قيمة معادة وليس بداية جسم الدالة.

const add = (a, b) => {
  return a + b;
};

const addShort = (a, b) => a + b;

متى تستخدم كائناً ومتى تستخدم دالة داخل setState()؟

يمكنك تمرير كائن مباشرة إلى setState() إذا كانت القيمة الجديدة لا تعتمد على القيمة السابقة.

class User extends React.Component {
  state = { name: "Mike" };

  handleChange = (event) => {
    const value = event.target.value;
    this.setState({ name: value });
  };

  render() {
    const { name } = this.state;

    return (
      <div>
        <input
          type="text"
          onChange={this.handleChange}
          placeholder="Enter your name"
          value={name}
        />
        <div>Hello, {name}</div>
      </div>
    );
  }
}

تحديث قيمة حقل الاسم في React باستخدام setState وتمرير كائن مباشر

في هذا المثال لا نحتاج إلى prevState، لأننا نأخذ القيمة مباشرة من الحقل النصي، لذلك تمرير كائن يكفي تماماً.

أما إذا كانت القيمة الجديدة تعتمد على القيمة الحالية، فالأفضل تمرير دالة لتجنّب النتائج غير الدقيقة الناتجة عن الطبيعة غير المتزامنة لـ setState().

مشكلة شائعة عند استخدام كائن داخل setState()

handleClick = () => {
  const { counter } = this.state;
  this.setState({ counter: counter + 1 });
};

المثال السابق يبدو سليماً، وغالباً سيعمل في الحالات البسيطة.

لكن لاحظ المثال التالي:

handleClick = () => {
  this.setState({ counter: 5 });
  const { counter } = this.state;
  this.setState({ counter: counter + 1 });
};

مثال على سلوك غير متوقع عند استخدام setState بكائن مع الاعتماد على قيمة قديمة

قد تتوقع أن تصبح القيمة 6، لكنها لا تفعل ذلك. السبب أن التحديث الأول لم يكتمل فوراً، ولذلك ما زلت تقرأ القيمة القديمة من this.state.

الحل الصحيح باستخدام prevState

handleClick = () => {
  this.setState({ counter: 5 });
  this.setState((prevState) => {
    return { counter: prevState.counter + 1 };
  });
  this.setState((prevState) => {
    return { counter: prevState.counter + 1 };
  });
};

دمج عدة استدعاءات setState بشكل صحيح باستخدام prevState في React

هنا ستُضبط القيمة أولاً إلى 5، ثم تزيد مرتين لتصبح 7. هذا يحدث لأن React تدمج التحديثات، وتستخدم prevState لحساب القيمة النهائية بدقة قبل إعادة العرض مرة واحدة.

الخلاصة العملية هنا:

  • استخدم كائناً إذا كانت القيمة الجديدة مستقلة.
  • استخدم دالة إذا كانت القيمة الجديدة مبنية على قيمة سابقة.

احذر من استخدام الحالة مباشرة بعد تحديثها

state = { isLoggedIn: false };

doSomethingElse = () => {
  const { isLoggedIn } = this.state;
  if (isLoggedIn) {
    // do something different
  }
};

handleClick = () => {
  this.setState({ isLoggedIn: true });
  doSomethingElse();
};

في هذا النمط، قد تتوقع أن تكون قيمة isLoggedIn قد أصبحت true مباشرة، لكن ذلك غير مضمون. لذلك يجب الانتباه دائماً عند تشغيل منطق يعتمد على الحالة مباشرة بعد استدعاء setState().

كيف تدمج React تحديثات الحالة تلقائياً؟

state = { counter: 0, username: "" };

handleOnClick = () => {
  this.setState((prevState) => ({ counter: prevState.counter + 1 }));
};

handleOnChange = (event) => {
  this.setState({ username: event.target.value });
};

عند تحديث counter لا تحتاج إلى إعادة تمرير username، والعكس صحيح. تقوم React بدمج خصائص الحالة داخلياً، ما يجعل التحديثات أبسط وأوضح.

تحديث مستقل لقيم متعددة داخل state مع دمج React للتغييرات تلقائياً

استخدام المكونات الدالية في React

قبل ظهور Hooks، كانت المكونات الدالية تُستخدم غالباً لعرض الواجهة فقط من دون حالة داخلية أو دورات حياة. كانت تستقبل props وتعيد JSX.

const User = (props) => {
  const { name, email } = props;
  const { first, last } = name;

  return (
    <div>
      <p>Name: {first} {last}</p>
      <p>Email: {email}</p>
      <hr />
    </div>
  );
};

في هذا المثال، المكون User يستقبل البيانات عبر props ويعرضها. لا نستخدم this هنا، وهذا أحد أسباب بساطة المكونات الدالية وسهولة اختبارها.

ومن المهم أن يبدأ اسم المكون بحرف كبير مثل User، لأن كتابة <user /> ستجعل React تتعامل معه كعنصر HTML عادي.

كيف تستخدم الحالة في المكونات الدالية مع React Hooks؟

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

أشهر Hook لإدارة الحالة هي useState.

مثال بصيغة المكونات الصنفية

import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
  state = { counter: 0 };

  handleOnClick = () => {
    this.setState(prevState => ({ counter: prevState.counter + 1 }));
  };

  render() {
    return (
      <div>
        <p>Counter value is: {this.state.counter}</p>
        <button onClick={this.handleOnClick}>Increment</button>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

التحويل إلى مكون دالي باستخدام useState

import React, { useState } from "react";
import ReactDOM from "react-dom";

const App = () => {
  const [counter, setCounter] = useState(0);

  return (
    <div>
      <div>
        <p>Counter value is: {counter}</p>
        <button onClick={() => setCounter(counter + 1)}>Increment</button>
      </div>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));

في هذا المثال:

  • استوردنا useState من React.
  • مررنا القيمة الابتدائية 0 إلى useState().
  • استخدمنا تفكيك المصفوفة array destructuring للحصول على:
  1. counter: القيمة الحالية.
  2. setCounter: الدالة المسؤولة عن التحديث.

عند الضغط على الزر، نستدعي setCounter(counter + 1) لتحديث القيمة وإعادة عرض المكون تلقائياً.

الميزة الأهم هنا أن الشيفرة أصبحت أقصر وأوضح، وهذا سبب رئيسي في تفضيل Hooks في المشاريع الحديثة.

أفضل ممارسات لفهم الحالة في React

  • لا تعدّل state مباشرة تحت أي ظرف.
  • استخدم setState() أو setCounter() لتحديث القيم.
  • إذا كانت القيمة الجديدة تعتمد على القيمة السابقة، استخدم دالة مع prevState.
  • تذكر أن setState() غير متزامنة، لذلك لا تعتمد على القيمة الجديدة فوراً في السطر التالي.
  • في المكونات الحديثة، يُفضَّل استخدام Functional Components مع Hooks.
  • حافظ على وضوح أسماء الحالة والدوال مثل counter وsetCounter.

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

فهم State في React هو المفتاح الحقيقي لبناء واجهات تفاعلية مستقرة وقابلة للتوسع. الفكرة الجوهرية ليست فقط في تخزين البيانات، بل في معرفة متى وكيف تُحدَّث هذه البيانات بالطريقة التي تفهمها React وتستجيب لها بكفاءة. إذا أتقنت الفرق بين التحديث المباشر والتحديث عبر setState، وفهمت الطبيعة غير المتزامنة للتحديثات، ثم انتقلت إلى استخدام useState بوعي داخل المكونات الدالية، فستمتلك أساساً قوياً جداً لتطوير تطبيقات React احترافية وأكثر قابلية للصيانة.

اترك تعليقاً

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