فصل ضمیمه: کار با useState – مشکل به‌روزرسانی

Appendix: Working with useState - Update Problem

22 بهمن 1399
فصل ضمیمه: کار با useState – مشکل به روز رسانی

در جلسه قبل توانستیم با موفقیت state خود را بر اساس input کاربر به روز رسانی کنیم اما مرتبا اخطارهایی در کنسول مرورگر دریافت می کردیم. هر زمان که در هر کدام از input ها چیزی می نوشتیم، در کنسول مرورگر خطایی مبنی بر تغییر input از نوع number می گرفتیم. در واقع نحوه استفاده ما از useState مشکلی ندارد اما نحوه به روز رسانی state ما است که باعث خطا می شود.

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

این موضوع در useState کاملا متفاوت است! در useState شیء پاس داده شده به طور کامل جایگزین شیء state می شود نه اینکه با آن ادغام شود. به کد زیر نگاه کنید:

onChange={event => inputState[1]({ title: event.target.value })}

بر اساس چیزی که گفتم با اجرای کد بالا، state ما دیگر شامل دو خصوصیت title و amount نیست بلکه یک شیء جدید است که فقط یک خصوصیت title دارد (شیء state قبلی حذف شده است). حالا احتمالا متوجه شده اید که چرا وقتی حتی در input اول تایپ می کردیم باز هم برای input دوم خطا دریافت می کردیم:

<div className="form-control">
    <label htmlFor="title">Name</label>
    <input
        type="text"
        id="title"
        value={inputState[0].title}
        onChange={event => inputState[1]({ title: event.target.value })}
    />
</div>
    <div className="form-control">
        <label htmlFor="amount">Amount</label>
        <input
            type="number"
            id="amount"
            value={inputState[0].amount}
            onChange={event => inputState[1]({ amount: event.target.value })}
        />
    </div>

در کد بالا که در جلسه قبل نوشته ایم، input دوم همیشه برای value خود به عضو اول inputState و سپس خصوصیت amount اشاره می کند. زمانی که در input اول چیزی تایپ کنیم، شیء state ما تغییر کرده و خصوصیت amount حذف می شود بنابراین آن خطا را دریافت می کنیم چرا که input دوم نمی تواند به value خود برسد.

به همین دلیل باید مطمئن شویم که هنگام به روز رسانی state، داده های قدیمی را از دست نمی دهیم. به عبارت دیگر هر بار که می خواهیم یک عضو از state را تغییر دهیم باید عضو دیگر را نیز روی state فعلی تنظیم کنیم:

<div className="form-control">
    <label htmlFor="title">Name</label>
    <input
        type="text"
        id="title"
        value={inputState[0].title}
        onChange={event => inputState[1]({ title: event.target.value, amount: inputState[0].amount })}
    />
</div>
    <div className="form-control">
        <label htmlFor="amount">Amount</label>
        <input
            type="number"
            id="amount"
            value={inputState[0].amount}
            onChange={event => inputState[1]({ amount: event.target.value, title: inputState[0].title })}
        />
    </div>

حالا اگر کدها را ذخیره کنید و به مرورگر بروید می توانید به راحتی در input ها تایپ کرده و هیچ خطایی دریافت نمی کنیم اما هنوز هم منطق کد ما مشکل دارد. نحوه کار useState طوری است که نمی توانیم صد در صد تضمین کنیم که قسمت اضافه شده در کد بالا آخرین نسخه از state ما است. به این کد نگاه کنید:

onChange={event => inputState[1]({ title: event.target.value, amount: inputState[0].amount })}

در این کد می خواهیم amount حذف نشود بنابراین آن را اضافه کرده ایم و مقدارش را برابر عضو اول از inputState و سپس خصوصیت amount قرار داده ایم اما هیچ تضمینی نیست که این مقدار برابر آخرین مقدار ثبت شده در state باشد. مثلا اگر در صفحه شما انیمیشن ها و کدهای زیادی در جریان باشد، react ممکن است بخواهد برخی از تغییرات state را به انتهای کد موکول کند و باعث مشکلاتی شود که من وارد آن نمی شوم. حتما باید توجه کنید که احتمال چنین اتفاقی بسیار نادر است و در برنامه ساده ای مثل برنامه ما هیچ وقت چنین اتفاقی نمی افتد اما از نظر فنی و در پروژه های بزرگ می تواند اتفاق بیفتد و برنامه شما را خراب کند بنابراین نباید ریسک کرد.

ما باید به جای اینکه داده های خودمان را به عضو دوم آرایه (که یک تابع بود) پاس بدهیم، باید یک تابع دیگر را به آن پاس بدهیم که داده های ما را return کند:

<label htmlFor="title">Name</label>
<input
  type="text"
  id="title"
  value={inputState.title}
  onChange={event => {
    inputState[1] (prevInputState => ({
      title: event.target.value,
      amount: prevInputState.amount
    }));
  }}
/>

همانطور که می بینید من یک تابع جدید را به به inputState پاس داده ام. prevInputState نیز به صورت خودکار به من پاس داده می شود و آخرین نسخه state ما را به ما می دهد تا به جای استفاده از inputState[0] از آن استفاده کنیم. تفاوت این مسئله اینجاست که prevInputState به جای برگرداندن state ذخیره شده، آخرین state ای که توسط توابع ما ثبت شده را به ما برمی گرداند بنابراین مشکلی که در موردش صحبت کردیم از بین می رود.

اگر کد بالا را ذخیره کرده و به مرورگر بروید، با تایپ در input ها به خطایی برمی خورید که می گوید از یک event چند بار استفاده کرده اید. مشکل اینجاست که ما در حال حاضر دو تابع داریم که تو در تو هستند: یکی تابع onChange که event را می گیرد و دیگری تابعی که برای به روز رسانی state درون onChange نوشته ایم. من اگر در تابع دوم از چیزی استفاده کنم که در تابع اول وجود دارد (همان event)، باعث مشکلی می شوم. در واقع با این کار event ما روی اولین کلیدی که توسط کاربر فشار داده شود قفل می شود! و از این به بعد دیگر کلیدها و نوشته های بعدی را نادیده می گیرد. بدین صورت ما از همان event استفاده کرده و خطا را دریافت می کنیم. این مشکل به دلیل رفتار react رخ می دهد. React به جای استفاده از event های اصلی مرورگر، از event های خودش استفاده می کند و به جای اینکه برای هر بار فشردن کلید توسط کاربر یک event جدید داشته باشیم همان event خودش را به روز رسانی می کند که باعث بروز این مشکل می شود. این مشکل واقعا پیچیده است و اگر زیاد متوجه آن نمی شوید مشکلی نیست. تنها انتظاری که از شما می رود این است که از این به بعد طبق راه حلی که به شما می دهم عمل کنید.

راه حل این است که event.target.value را درون تابع اول و خارج از تابع دوم قرار دهید:

<div className="form-control">
    <label htmlFor="title">Name</label>
    <input
        type="text"
        id="title"
        value={inputState.title}
        onChange={event => {
            const newTitle = event.target.value;
            inputState[1](prevInputState => ({
                title: newTitle,
                amount: prevInputState.amount
            }));
        }}
    />
</div>
    <div className="form-control">
        <label htmlFor="amount">Amount</label>
        <input
            type="number"
            id="amount"
            value={inputState.amount}
            onChange={event => {
                const newAmount = event.target.value;
                inputState[1](prevInputState => ({
                    amount: newAmount,
                    title: prevInputState.title
                }));
            }}
        />
    </div>

همانطور که در کد بالا می بینید من event.target.value را درون const هایی خارج از تابع دوم قرار داده ام (newAmount و newTitle) و بدین صورت مشکل ما حل خواهد شد. البته برای راحت تر شدن کار با بقیه موارد می توانیم از array destructuring استفاده کنیم. در حال حاضر پاسخ useState را درون متغیری به نام inputState قرار می دهیم اما می توانیم آن را بدین شکل انجام بدهیم:

  const [inputState, setInputState] = useState({ title: '', amount: '' });

ما می دانیم که useState یک آرایه با دو عضو برمی گرداند بنابراین می توانیم جلوی const یک آرایه با دو عضو ایجاد کنیم و نام های دلخواهمان را به آن ها بدهیم. این کار باعث می شود که دو متغیر (نه آرایه) جدید داشته باشیم که اولی برابر با عضو اول آرایه برگشتی و دومی برابر با عضو دوم آرایه برگشتی باشد. حالا می توانیم کد خود را راحت تر بنویسیم:

          <div className="form-control">
            <label htmlFor="title">Name</label>
            <input
              type="text"
              id="title"
              value={inputState.title}
              onChange={event => {
                const newTitle = event.target.value;
                setInputState(prevInputState => ({
                  title: newTitle,
                  amount: prevInputState.amount
                }));
              }}
            />
          </div>
          <div className="form-control">
            <label htmlFor="amount">Amount</label>
            <input
              type="number"
              id="amount"
              value={inputState.amount}
              onChange={event => {
                const newAmount = event.target.value;
                setInputState(prevInputState => ({
                  amount: newAmount,
                  title: prevInputState.title
                }));
              }}
            />
          </div>
تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری دوره جامع آموزش ری اکت توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

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