إدارة الحالة

Intermediate

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

الاستجابة للمدخلات باستخدام الحالة

باستخدام React، لن تستطيع تعديل واجهة المستخدم (UI) عن طريق الكود مباشرة. على سبيل المثال، لن تكتب أوامر مثل “عطل الزر”، “فعل الزر”، “أظهر رسالة النجاح”، إلخ. بدلا عن ذلك، سوف تصف واجهة المستخدم التي تريد أن تراها للحالات المرئية من مكوناتك (“حالة ابتدائية (initial state)”، “حالة كتابية (typing state)”، “حالة ناجحة (success state)”)، ومن بعدها تنشيط تغيرات الحالة بناء على مدخل المستخدم. هذا مشابه لتصور المصممين عن واجهة المستخدم.

هنا نموذج اختبار صمم باستخدام React. لاحظ كيف يستخدم متغير الحالة status لكي يحدد ما إذا سيفعل أم سيعطل زر الإرسال، وما إذا ستظهر رسالة نجاح بدلا عن ذلك.

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>هذا صحيح!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>اختبار المدينة</h2>
      <p>
        في أي مدينة يوجد لوحة إعلانية تقوم بتحويل الهواء إلى مياه صالحة للشرب؟
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          أرسل
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // محاكاة للتواصل باستخدام الشبكة
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima' 
      if (shouldError) {
        reject(new Error('توقع جيد ولكن إجابة خاطئة. حاول مرة أخرى!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

Ready to learn this topic?

اقرأ الاستجابة للمدخلات باستخدام الحالة لكي تتعلم كيفية التعامل مع التفاعلات بعقلية موجّهة بناء على الحالة.

Read More

اختيار هيكل الحالة

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

على سبيل المثال، هذا نموذج يتضمن متغير الحالة fullName مكرر:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>دعنا نقم بتسجيلك</h2>
      <label>
        الاسم الأول:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        اسم العائلة:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        تذكرتك سوف تسلم لـ: <b>{fullName}</b>
      </p>
    </>
  );
}

يمكنك حذفه وتبسيط الكود عن طريق جمع fullName بينما يُصيّر المكوّن:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>دعنا نقم بتسجيلك</h2>
      <label>
        الاسم الأول:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        اسم العائلة:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        تذكرتك سوف تسلم لـ: <b>{fullName}</b>
      </p>
    </>
  );
}

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

Ready to learn this topic?

اقرأ اختيار هيكل الحالة لتتعلم كيفية تصميم بنية الحالة لتجنب الأخطاء.

Read More

مشاركة الحالة بين المكونات

أحيانا، تريد حالة مكونين أن تتغير دائما مع بعضها البعض. لعمل ذلك، احذف الحالة من كليهما، وانقلها لأقرب مكون أب مشترك، وبعد ذلك مررها لأسفل باستخدام الخصائص (props). هذا ما يعرف بـ “رفع الحالة لمستوى أعلى (lifting state up)”، وهو واحد من أكثر الأشياء شيوعا التي ستستعملها أثناء كتابتك لكود React.

في هذا المثال، في كل مرة يجب أن تكون قائمة واحدة فقط نشطة. لتحقيق ذلك، بدلا من حفظ الحالة النشطة داخل كل قائمة بمفردها، المكونّ الأب يحمل الحالة ويحدد الخصائص لمكوناته الأبناء.

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>ألماتي، كازاخستان</h2>
      <Panel
        title="نبذة"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        مع تعداد سكاني يقارب 2 مليون، ألماتي هي أكبر مدينة بكازاخستان. منذ 1929 إلى 1997 كانت هي العاصمة.
      </Panel>
      <Panel
        title="أصل الكلمة"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        الاسم مأخوذ من <span lang="kk-KZ">алма</span>، الكلمة الكازاخية التي تعني "تفاحة" وغالبا تترجم على أنها "مليئة بالتفاح". في الحقيقة، المنطقة المحيطة بألماتي تعتبر الموطن الأصلي للتفاح، والنوع البريّ <i lang="la">Malus sieversii</i> يعتبر أقرب مرشح لكونه أصل للتفاح المحلي الحديث.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          أظهر
        </button>
      )}
    </section>
  );
}

Ready to learn this topic?

اقرأ مشاركة الحالة بين المكونات لتتعلم كيفية رفع الحالة لمستوى أعلى والحفاظ على المكونّات منسجمة.

Read More

حفظ وإعادة تعيين الحالة

عندما تعيد تصيير مكون، React تحتاج لتقرر أىّ أجزاء شجرة المكونات تحفظها (وتحدثها)، وأيّ أجزاءها تلغيها أو تعيد إنشاءها من الصفر. في أغلب الحالات، التصرف التلقائي لـ React يعمل بشكل جيد كفاية. تحفظ React تلقائيًا أجزاء الشجرة التي “تتوافق” مع مكون الشجرة المصيّر مسبقا.

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

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'علي', email: 'ali12@mail.com' },
  { name: 'هند', email: 'hend@mail.com' },
  { name: 'سعد', email: 'sa2d@mail.com' }
];

تعطيك React القدرة على تجاوز السلوك الافتراضي، وإجبار المكون إعادة تعيين حالته عن طريق تمرير key مختلف لها، مثل <Chat key={email} />. هذا يخبر React أن الطرف المستقبل مختلف، ومن الواجب تعيين مكون Chat مختلف يكون بحاجة إلى إعادة الإنشاء من الصفر ببايانات جديدة (وواجهة مستخدم مطابقة للمدخلات). الآن الانتقال بين المستقبلين يعيد تعيين حقل الإدخال — حتى بالرغم من أنك تعيد تصيير نفس المكون.

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.email} contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'علي', email: 'ali12@mail.com' },
  { name: 'هند', email: 'hend@mail.com' },
  { name: 'سعد', email: 'sa2d@mail.com' }
];

Ready to learn this topic?

اقرأ حفظ وإعادة تعيين الحالة لتتعلم عن الحياة الزمنية للحالة وكيفية التحكم بها.

Read More

استخلاص منطق الحالة إلى مخفض (reducer)

المكونات ذات تحديثات حالة كثيرة المنتشرة خلال كثير من معالجات الأحداث (event handlers) قد تصبح معقدة. لمثل هذه الأحوال، يمكنك تجميع جميع منطق تحديث الحالة خارج مكوّنك داخل دالة واحدة، تدعى “مخفض (reducer)“. معالجات الأحداث خاصتك ستصبح موجزة لأنها تحدد “إجراءات (actions)” المستخدم فقط. في أسفل الملف، دالة المخفض تحدد كيف يجب أن تحدث الحالة استجابةً لكل إجراء!

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>مخطط رحلة Prague</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'زيارة متحف Kafka', done: true },
  { id: 1, text: 'مشاهدة عرض الدمى', done: false },
  { id: 2, text: 'صورة Lennon Wall', done: false }
];

Ready to learn this topic?

اقرأ استخلاص منطق الحالة إلى مخفض (reducer) لتتعلم كيفية تجميع منطق داخل دالة المخفض.

Read More

تمرير البيانات إلى عمق باستخدام السياق (context)

عادة، سوف تقوم بتمرير معلومات من مكوّن أب إلى مكوّن ابن بواسطة الخصائص (props). لكن تمرير الخصائص قد يكون غير مجدٍ لو احتجت لتمرير بعض الخصائص خلال مكونات عديدة، أو لو أن العديد من المكونات تحتاج نفس المعلومات. السياق (context) يتيح للمكون الأب جعل بعض المعلومات متوفرة لأي مكون أدناه في الشجرة -لا يهم مقدار عمق المكون- بدون تمريرها مباشرة عن طريق الخصائص.

هنا، المكوّن Heading يحدد مستوى عنوانه عن طريق “سؤال” أقرب Section لمستواه. كل Section يتتبع مستواه الخاص عن طريق سؤال Section الأب وإضافة واحد له. كل Section يقوم بتوفير معلومات لجميع المكونات أدناه دون نقل الخصائص — وهذا يتم عبر السياق (context).

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>عنوان</Heading>
      <Section>
        <Heading>عنوان رئيسي</Heading>
        <Heading>عنوان رئيسي</Heading>
        <Heading>عنوان رئيسي</Heading>
        <Section>
          <Heading>عنوان فرعي</Heading>
          <Heading>عنوان فرعي</Heading>
          <Heading>عنوان فرعي</Heading>
          <Section>
            <Heading>عنوان فرعي ثانٍ</Heading>
            <Heading>عنوان فرعي ثانٍ</Heading>
            <Heading>عنوان فرعي ثانٍ</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

Ready to learn this topic?

اقرأ تمرير البيانات إلى عمق باستخدام السياق (context) لتتعلم عن استخدام السياق (context) كبديل لتمرير الخصائص.

Read More

التوسع بواسطة المخفض (reducer) و السياق (context)

المخفضات (Reducers) تتيح لك تجميع منطق تحديث الحالة لمكون. السياق (Context) يتيح لك تمرير معلومات بعمق إلى أسفل لمكونات أخرى. يمكنك جمع المخفضات والسايق معا لتدير الحالة الخاصة بشاشة معقدة.

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

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>يوم إجازة في Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

Ready to learn this topic?

اقرأ التوسع بواسطة المخفض (reducer) و السياق (context) لتتعلم كيف توسيع إدارة الحالة في تطبيق نامٍ.

Read More

ماذا بعد ذلك؟

توجه إلى الاستجابة للمدخلات باستخدام الحالة لبدء قراءة هذا الفصل صفحة بصفحة!

أو، إذا كنت بالفعل على دراية بهذه المواضيع، لماذا لا تقرأ عن بوابات الهروب (Escape Hatches)؟