فصل ضمیمه: چندین state همزمان + اضافه کردن محتویات به فرم

Appendix: Multiple Simultaneously States

22 بهمن 1399
فصل ضمیمه: چندین state همزمان + اضافه کردن محتویات به فرم

چندین state همزمان

امیدوارم با مطالعه قسمت های قبلی متوجه نحوه کار useState شده باشید. در حال حاضر state ما فقط دو خصوصیت title و amount را دارد و آنقدرها پیچیده نیست اما من روش به روز رسانی State خودمان را دوست ندارم چرا که اصلا بهینه نیست. ما هر بار که بخواهیم یکی از خصوصیات را تغییر دهیم باید حواسمان باشد که خصوصیت دیگر را نیز مقداردهی صحیح کنیم. شاید در چنین state ای که فقط دو خصوصیت دارد مشکلی نداشته باشیم اما در برنامه های واقعی که state های بزرگی داریم مدیریت چنین مواردی بسیار سخت خواهد بود.

در state های موجود در کامپوننت های کلاس محور، state باید شیء باشد و خود react کار حفظ state قبلی را انجام می دهد اما در useState اینچنین نیست بلکه می توانید به جای یک state چندین state داشته باشید! بنابراین به جای اینکه کل state برنامه را در یک شیء قرار بدهیم می توانیم هر خصوصیت را در یک State جداگانه قرار بدهیم:

const IngredientForm = React.memo(props => {
  const [enteredTitle, setEnteredTitle] = useState('');
  const [enteredAmount, setEnteredAmount] = useState('');
// بقیه کدها //

بنابراین حالا دو state جداگانه داریم (اولی برای title و دومی برای amount) بنابراین اگر بخواهم یکی از آن ها را به روز رسانی کنم دیگر نیازی به مدیریت state های دیگر نیست چرا که از یکدیگر مستقل هستند. حالا کار ما برای تغییر state بسیار راحت تر می شود:

<label htmlFor="title">Name</label>
<input
  type="text"
  id="title"
  value={enteredTitle}
  onChange={event => {
    setEnteredTitle(event.target.value);
  }}
/>

در واقع در value مقدار enteredTitle را گذاشته ام و به جای آن تابع بلند و بالا برای به روز رسانی State از setEnteredTitle استفاده کرده ام که به تنهایی مشکل ما را حل می کند. همین کار را برای amount نیز انجام می دهیم:

<label htmlFor="amount">Amount</label>
<input
  type="number"
  id="amount"
  value={enteredAmount}
  onChange={event => {
    setEnteredAmount(event.target.value);
  }}
/>

به همین دلیل همیشه پیشنهاد می کنم در هنگام کار با useState از چندین State مختلف استفاده کنید نه اینکه همه را در یک شیء یا آرایه قرار دهید. چند قسمتی را روی useState صرف کردیم چرا که اهمیت useState بسیار بالا بود. نهایتا اگر بخواهیم useState را خلاصه کنیم می گوییم:

useState در کامپوننت های کاربردی استفاده می شود و هرگاه بخواهیم که state جدیدی را تعریف کنیم باید useState را صدا بزنیم. پارامتر ورودی useState همان initial state یا state اولیه شما خواهد بود. البته مجبور به پاس دادن پارامتر ورودی (مثل رشته و عدد در کد ما) نیستید بلکه می توانید هیچ پارامتری را پاس ندهید تا state اولیه شما null باشد. با این کار state جدیدی برای شما ساخته می شود که توسط react مدیریت می شود و همچنین با re-render شدن صفحه از بین نمی رود. پس از آن یک آرایه با دو عضو به شما برگردانده می شود که اولی به state شما اشاره می کند و دومی یک تابع است که مسئولیت به روز رسانی تابع شما را بر عهده دارد. این خلاصه عملکرد useState است.

دو قانون مهم در هنگام کار با hook ها وجود دارد (نه فقط useState) که همیشه باید رعایت کنید:

  • ما تنها زمانی از hook ها استفاده می کنیم که درون یک کامپوننت کاربردی (و نه کلاس-محور) و یا درون یک hook سفارشی (که خودمان برنامه نویسی کنیم) باشیم.
  • همیشه از hook ها در root level استفاده کنید. یعنی نمی توانید از hook ها درون توابع تو در تو استفاده کنید بلکه باید آن ها را در Scope اصلی کامپوننت خود بیاورید. به طور مثال در کدهای ما در فایل js یک تابع به نام submitHandler داریم. شما اجازه ندارید useState را درون آن صدا بزنید. این قانون شامل if ها و حلقه ها نیز می شود و نمی توانید hook ها را درون آن ها صدا بزنید.

اضافه کردن محتویات به فرم

حالا که مروری بر useState داشتیم باید به پروژه خودمان برگردیم و کاری کنیم که با فشرده شدن دکمه فرم (در فایل HTML) که Add Ingredient نام داشت، ingredient خودمان را در جایی ثبت کنیم. من این کار را در فایل Ingredients.js انجام می دهم. من کامپوننت درون این فایل را به صورت تابع عادی با کلیدواژه Function نوشته بودم تا بدانید می توانید به این روش هم عمل کنید اما برای گیج نشدن شما از همان arrow function ها استفاده می کنیم:

const Ingredients = () => {
// بقیه کدها //

اولین قدم وارد کردن useState درون این فایل است:

import React, { useState } from 'react';

حالا می توانیم از useState استفاده کنیم تا یک State جدید به وجود بیاوریم:

  const [userIngredients, setUserIngredients] = useState([]);

من state اولیه خودم را یک آرایه خالی گذاشته ام چرا که ingredients ما (محتویات) قرار است یک آرایه باشد. شما می توانید به سلیقه خود آن را تغییر دهید ولی کار با آرایه راحت تر است. در مرحله بعد باید کامپوننت IngredientList را نیز وارد این فایل کنیم:

import IngredientList from './IngredientList';

و آن را در قسمت return به شکل زیر اضافه کنیم (قسمت مورد نظر را برایتان کامنت کرده ام):

  return (
    <div className="App">
      <IngredientForm />

      <section>
        <Search />
        <IngredientList />
      </section>
    </div>
  );

اگر به فایل IngredientsList.js بروید متوجه می شوید که این کامپوننت یک prop به نام ingredients می گیرد که درونش به جز title و amount یک id نیز می خواهیم. منظورم قسمت زیر است:

  return (
    <section className="ingredient-list">
      <h2>Loaded Ingredients</h2>
      <ul>
        {props.ingredients.map(ig => (
          <li key={ig.id} onClick={props.onRemoveItem.bind(this, ig.id)}>
            <span>{ig.title}</span>
            <span>{ig.amount}x</span>
          </li>
        ))}
      </ul>
    </section>
  );

بنابراین باید در فایل خودمان (Ingredients.js) نیز این prop را پاس بدهیم. من مقدار ingredient را روی ingredientList روی همان مقدار userIngredients قرار می دهم که state من است:

بنابراین باید در فایل خودمان (Ingredients.js) نیز این prop را پاس بدهیم. من مقدار ingredient را روی ingredientList روی همان مقدار userIngredients قرار می دهم که state من است:

        <IngredientList ingredients={userIngredients} />

حالا کامپوننت ingredientForm ما باید بتواند ingredient های جدیدی را اضافه کند بنابراین باید برایش یک متد تعریف کنیم تا بتواند چنین کاری را انجام دهد. این متد یک ingredient جدید را دریافت می کند و سپس آن را درون آرایه خودمان قرار می دهد:

  const addIngredientHandler = ingredient => {
    setUserIngredients();
  };

در مرحله بعد باید ingredient جدید خودمان را درون setUserIngredients قرار دهیم تا state ما را به روز رسانی کند. نکته مهم اینجاست که در این مرحله راه میان بری وجود ندارد و باید ingredient های قبلی را حفظ کنیم، بنابراین باید از روش تابعی قبلی استفاده کنیم تا مطمئن شویم آخرین نسخه state را دریافت می کنیم:

  const addIngredientHandler = ingredient => {
    setUserIngredients(prevIngredients => [
      ...prevIngredients,
      { id: Math.random().toString(), ...ingredient }
    ]);
  };

ما این ساختار را قبلا با هم کار کردیم. prevIngredients در واقع آخرین نسخه از state فعلی ما است. ما این مقدار را با اپراتور spread از هم باز می کنیم (تمام جفت های key/value را از درون آرایه بیرون می آوریم) تا ingredient های قبلی حذف نشوند و در state جدید حضور داشته باشند. سپس برای تولید id از math.random استفاده کرده ایم چرا که فعلا وب سروری نداریم که بخواهید id را به صورت خودکار برایمان تولید کند. در نهایت با استفاده از اپراتور spread روی ingredient (که پارامتر ورودی این تابع است)، مقادیر title و amount را نیز دریافت می کنیم. در واقع فرض ما این است که ingredient که به این تابع پاس داده می شود یک شیء خواهد بود و با خودمان قرار گذاشته ایم که هنگام پاس دادن این مقدار از یک شیء استفاده کنیم.

در مرحله بعد این تابع ساخته شده را به IngredientForm پاس می دهم:

  return (
    <div className="App">
      <IngredientForm onAddIngredient={addIngredientHandler} />
// بقیه کدها //

نام این prop را onAddIngredient گذاشته ام اما شما می توانید هر نامی که دوست داشتید را انتخاب کنید. حالا به راحتی به فایل IngredientForm.js می رویم و از این تابع پاس داده شده درون submitHandler استفاده می کنیم:

  const submitHandler = event => {
    event.preventDefault();
    props.onAddIngredient({ title: enteredTitle, amount: enteredAmount });
  };

دلیل این کار هم واضح است. ما در فایل IngredientForm.js فرم خود را داریم که و این فرم در هنگام ثبت، submitHandler را اجرا خواهد کرد:

        <form onSubmit={submitHandler}>

در حال حاضر کدهای ما تکمیل نشده است. می دانید چرا؟ به کد زیر از فایل IngredientsList.js نگاه کنید:

  return (
    <section className="ingredient-list">
      <h2>Loaded Ingredients</h2>
      <ul>
        {props.ingredients.map(ig => (
          <li key={ig.id} onClick={props.onRemoveItem.bind(this, ig.id)}>
            <span>{ig.title}</span>
            <span>{ig.amount}x</span>
          </li>
        ))}
      </ul>
    </section>
  );

ما در این قسمت مشخص کرده ایم که هنگام click روی یکی از <li> ها، که ingredient های ما را درون خودشان نگه می دارند، باید تابعی اجرا شود و آن <li> را حذف کند. تکمیل این کد را در جلسات بعد انجام می دهیم اما اگر می خواهید قسمت دیگر برنامه را تست کنید می توانید از یک حقه استفاده کنیم.

اگر به فایل Ingredients.js برویم و یک تابع خالی برای حذف <li> ها پاس بدهیم مشکلی نخواهیم داشت:

  return (
    <div className="App">
      <IngredientForm onAddIngredient={addIngredientHandler} />

      <section>
        <Search />
        <IngredientList ingredients={userIngredients} onRemoveItem={() => {}} />
      </section>
    </div>
  );

البته با پاس دادن یک تابع خالی <li> ها حذف نمی شوند اما دیگر به خطایی هم برنمی خوریم. حالا می توانید به مرورگر بروید و از فرم استفاده کنید. مثلا من Apples را به تعداد 5 می نویسم و روی Add Ingredient کلیک می کنم. نتیجه به خوبی نمایش داده می شود:

فرم به درستی محتویات را به < li > ها اضافه می کندفرم به درستی محتویات را به

    • ها اضافه می کند

البته هنوز <li> ها حذف نمی شوند.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری دوره جامع آموزش ری اکت توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.