صف بندی مجموعه ای از بروزرسانی های State

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

You will learn

  • مفهوم “دسته‌بندی” چیست و چطور ری‌اکت با استفاده از آن چندین بروزرسانی state را پردازش می‌کند
  • چطور می‌توان چند بروزرسانی یک متغییر state را طی یک رندر انجام داد

بروزرسانی‌های دسته‌ای state در ری‌اکت

ممکن است انتظار داشته باشید که با کلیک بر روی دکمه “+3” شمارنده سه واحد افزایش یابد زیرا setNumber(number + 1) را سه بار فراخوانی می‌کند:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

با این حال، همانطور که ممکن است از بخش قبل به خاطر داشته باشید، در هر رندر مقدار state ثابت است، بنابراین مقدار number همیشه در اولین رندرِ حاصل از اجرای کنترل رویداد کلیک 0 است، فارغ از این که چند بار setNumber(1) را فراخوانی کنید:

setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);

اما لازم است در اینجا به یک نکته‌ی دیگر توجه داشته باشیم. ری‌اکت پردازش بروزرسانی state را بعد از اجرای تمام کدهای داخل کنترل کننده‌ی رویداد انجام می‌دهد. به همین دلیل است که رندر مجدد تنها بعد از تمام فراخوانی‌های setNumber() اتفاق می‌افتد.

این ممکن است شما را به یاد گارسونی بیاندازد که در رستوران در حال گرفتن یک سفارش است. گارسون با ذکر اولین غذا توسط شما به سمت آشپزخانه نمی‌دود! در عوض، آن‌ها به شما اجازه می‌دهند که شفارشتان را تمام کنید، آن را تغییر دهید، و حتی سفارش سایر افراد حاضر در میز را نیز دریافت می‌کنند.

An elegant cursor at a restaurant places and order multiple times with React, playing the part of the waiter. After she calls setState() multiple times, the waiter writes down the last one she requested as her final order.

Illustrated by Rachel Lee Nabors

این به شما این امکان را می‌دهد که چندین متغییر state را بدون انجام رندرهای مجدد زیاد بروزرسانی کنید—حتی از چندین کامپوننت متفاوت—. اما باعث این هم می‌شود که UI تا بعد از اجرای کامل کنترل کننده رویداد، و تمام کدهای داخل آن بروزرسانی نشود. این رفتار، که به عنوان دسته‌بندی نیز شناخته می‌شود، باعث می‌شود برنامه‌ی ری‌اکت شما خیلی سریع‌تر اجرا شود. همچنین از مواجه شدن با بروزرسانی‌های “نیمه کاره‌ی” گیج کننده که در آن‌ها فقط برخی متغییر‌ها بروزرسانی می‌شوند جلوگیری می‌کند.

ری‌اکت مدیریت چند رویداد مانند چند بار کلیک کردن عمدی و پشت سر هم را دسته‌بندی نمی‌کند—مدیریت هر رویداد به صورت جداگانه انجام می‌شود. مطمئن باشید ری‌اکت تنها زمانی دسته‌بندی را انجام می‌دهد که استفاده از آن کاملا ایمن باشد. این به عنوان مثال تضمین می‌کند که اگر اولین کلیک یک فرم را غیر فعال کرد، کلیک دوم آن را دوباره ارسال نکند.

انجام چند بروزرسانی روی یک state قبل از رندر بعدی

این یک استفاده‌ی غیر معمول است، اما اگر می‌خواهید یک متغییر state را قبل از رندر بعدی، چند بار بروزرسانی کنید، به جای ارسال مقدار بعدی state مثل setNumber(number + 1)، می‌توانید تابعی را ارسال کنید تا مقدار بعدی state را بر اساس مقدار قبلی آن که در صف هست محاسبه کند، مثل setNumber(n => n + 1). این راهی است تا به ری‌اکت بگوییم “با مقدار state کاری انجام بده” به جای آن که جایگزینش کنی.

حالا شمارنده را افزایش دهید:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(n => n + 1);
        setNumber(n => n + 1);
        setNumber(n => n + 1);
      }}>+3</button>
    </>
  )
}

در اینجا n => n + 1 یک تابع بروزرسانی نامیده می‌شود. زمانی که شما آن را به یک تنظیم کننده‌ی state ارسال می‌کنید:

  1. ری‌اکت این تابع را در صف قرار می‌دهد تا پس از اجرای سایر کدهای کنترل کننده‌ی رویداد پردازش شود.
  2. در رندر بعدی ری‌اکت صف را تا اتنها پیمایش می‌کند و state آپدیت شده‌ی نهایی را به شما می‌دهد.
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

در اینجا نحوه‌ی عملکرد ری‌اکت، هنگام پیمایش این خطوط کد که در کنترل کننده‌ی رویداد اجرا می‌شوند آمده است:

  1. setNumber(n => n + 1): n => n + 1 یک تابع است. ری‌اکت آن را به یک صف اضافه می‌کند.
  2. setNumber(n => n + 1): n => n + 1 یک تابع است. ری‌اکت آن را به یک صف اضافه می‌کند.
  3. setNumber(n => n + 1): n => n + 1 یک تابع است. ری‌اکت آن را به یک صف اضافه می‌کند.

زمانی که شما useState را فراخوانی می‌کنید، در رندر بعدی، ری‌اکت به صف مراجعه می‌کند. مقدار قبلی number برابر 0 است، پس این همان چیزی است که ری‌اکت به عنوان اولین مقدار به عنوان آرگومان n به تابع بروزرسانی ارسال می‌کند. سپس ری‌اکت مقدار بازگشتی از تابع بروزرسانی را دریافت می‌کند و آن را به عنوان آرگومان nبه تابع بروزرسانی بعدی ارسال می‌کند، و به همین ترتیب تا اتنها ادامه می‌دهد:

queued updatenreturns
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

ری‌اکت 3 را به عنوان مقدار نهایی ذخیره می‌کند و آن را از useState برمی‌گرداند.

به همین دلیل است که کلیک کردن روی دکمه‌ی “+3” در مثال بالا مقدار را به درستی 3 واحد افزایش می‌دهد.

چه اتفاقی خواهد افتاد اگر state را بعد از جایگزینی بروزرسانی کنید

در مورد کنترل کننده‌ی زیر چطور؟ به نظر شما در رندر بعدی number چه مقداری خواهد داشت؟

<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
      }}>Increase the number</button>
    </>
  )
}

چیزی که کنترل کننده‌ی رویداد در مثال بالا به ری‌اکت می‌گوید انجام بدهد به صورت زیر است:

  1. setNumber(number + 5): مقدار number برابر 0 است، بنابراین setNumber(number + 5) همان
    setNumber(0 + 5) است و ری‌اکت “جایگزینی با 5 را به صف اضافه می‌کند.
  2. setNumber(n => n + 1): n => n + 1 یک تابع بروزرسانی است. ری‌اکت این تابع را به صف اضافه می‌کند.

در رندر بعدی، ری‌اکت صف state را پیماش می‌کند:

queued updatenreturns
”replace with 50 (unused)5
n => n + 155 + 1 = 6

ری‌اکت مقدار 6 را به عنوان نتیجه‌ی نهایی ذخیره می‌کند و آن را از useState بر می‌گرداند.

Note

شاید متوجه شده باشید که setState(5) در واقع شبیه setState(n => 5) عمل می‌کند، اما n در آن استفاده نشده است!

چه اتفاقی خواهد افتاد اگر state را بعد از بروزرسانی با تابع، جایگزین کنید

بیایید یک مثال دیگر را امتحان کنیم. به نظر شما در رندر بعدی number چه مقداری خواهد داشت؟

<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
        setNumber(42);
      }}>Increase the number</button>
    </>
  )
}

در زیر نحوه‌ی عملکرد ری‌اکت هنگام اجرای کدهای تابع کنترل کننده‌ی رویداد مثال بالا آمده است:

  1. setNumber(number + 5): مقدار number برابر 0 است، پس setNumber(number + 5) با setNumber(0 + 5) یکسان است. ری‌اکت “جایگزینی با 5 را به صف این state اضافه می‌کند.
  2. setNumber(n => n + 1): n => n + 1 یک تابع بروزرسانی است. ری‌اکت این تابع را به صف اضافه می‌کند.
  3. setNumber(42): ری‌اکت “جایگزینی با 42 را به صف اضافه می‌کند.

در رندر بعدی, ری‌اکت صف state را پیمایش می‌کند:

queued updatenreturns
”replace with 50 (unused)5
n => n + 155 + 1 = 6
”replace with 426 (unused)42

سپس ری‌اکت 42 را به عنوان نتیجه نهایی ذخیره می‌کند و آن را از useState برمی‌گرداند.

به طور خلاصه، پارامتری که به setNumber ارسال می‌کنید، به یکی از دو حالت زیر منجر خواهد شد:

  • یک تابع بروزرسانی: (به عنوان مثال n => n + 1) به صف اضافه می شود.
  • هر مقدار دیگری: (به عنوان مثال عدد 5) سبب اضافه شدن “جایگزینی با 5” به صف می‌شود و هر آنچه از قبل در صف قرار گرفته است را بی‌اثر می‌کند.

پس از اجرای کنترل کننده‌ی رویداد، ری‌اکت یک رندر مجدد را راه انداری می‌کند. طی این رندر مجدد ، ری‌اکت صف را پردازش خواهد کرد. تابع بروزرسانی در حین رندر اجرا می‌شود، بنابراین توابع بروزرسانی باید خالص باشند و فقط نتیجه را برگردانند. سعی نکنید state را از داخل توابع بروزرسانی مقداردهی کنید یا از سایر کنترل کننده‌های جانبی مانند useState()‍ استفاده کنید. در حالت Strict Mode، ری‌اکت هر تابع بروزرسانی را دو بار اجرا می‌کند(اما نتیجه دوم را نادیده می‌گیرد) تا به شما در یافتن اشتباهات کمک کند.

قرارداد‌های نام‌گذاری

معمولا آرگومان تابع بروزرسانی را با حروف اول متغییر state مربوطه نام‌گذاری می‌کنند:

setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);

اگر کد طولانی‌تر را ترجیح می‌دهید، یکی دیگر از شیوه‌های رایج، تکرار نام کامل متغییر state است، مثلا setEnabled(enabled => !enabled)، یا استفاده از پیشوند مثل setEnabled(prevEnabled => !prevEnabled).

Recap

  • بروزرسانی یک state مقدار آن را در رندر فعلی تغییر نمی‌دهد، اما برای یک رندر جدید درخواست می‌دهد.
  • ری‌اکت بروزرسانی state را پس از پایان اجرای کنترل کننده‌های رویداد پردازش می‌کند، به این دسته‌بندی می‌گویند.
  • برای چند بار بروزرسانی یک state در وقوع یک رویداد می‌توانید از تابع بروزرسانی مثل setNumber(n => n + 1) استفاده کنید.

Challenge 1 of 2:
شمارنده درخواست را درست کنید

شما در حال کار روی یک برنامه‌ی بازار هنری هستید که به کاربر امکان می‌دهد چندین سفارش را برای یک کالای هنری به طور همزمان ارسال کند. هر بار که کاربر دکمه‌ی “خرید” را فشار می‌دهد, شمارنده‌ی “در انتظار” باید یک واحد افزایش یابد. بعد از سه ثانیه باید شمارنده‌ی “در انتظار” کاهش یابد و شمارنده‌ی “کامل شده” افزایش یابد.

با این حال شمارنده‌ی “در انتظار” طوری که مورد نظر ما است رفتار نمی‌کند. وقتی “خرید” را فشار می‌دهید، به -1 کاهش می‌یابد (که نباید امکان‌پذیر باشد!). و اگر دو بار سریع کلیک کنید، شاهد رفتار غیرقابل پیش‌بینی از هر دو شمارنده خواهید بود.

چرا این اتفاق می‌افتد؟ هر دو شمارنده را درست کنید.

import { useState } from 'react';

export default function RequestTracker() {
  const [pending, setPending] = useState(0);
  const [completed, setCompleted] = useState(0);

  async function handleClick() {
    setPending(pending + 1);
    await delay(3000);
    setPending(pending - 1);
    setCompleted(completed + 1);
  }

  return (
    <>
      <h3>
        Pending: {pending}
      </h3>
      <h3>
        Completed: {completed}
      </h3>
      <button onClick={handleClick}>
        Buy     
      </button>
    </>
  );
}

function delay(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}