صف کردن چندین بروزرسانی وضعیت در React

mohsen1 ماه قبل
ارسال شده در
react/docs/v19

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

React به‌روزرسانی‌های وضعیت را دسته‌بندی می‌کند

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

مشاهده کد و اجرای آن در CodeSandbox

      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>
    </>
  )
}
    

با این حال، همان‌طور که ممکن است از بخش قبلی به خاطر داشته باشید، مقادیر وضعیت هر رندر ثابت هستند، مقدار number در داخل تابع رویداد اولین رندر همیشه 0 است، صرف‌نظر از اینکه چند بار setNumber(1) را فراخوانی می‌کنید، همان صفر باقی می ماند:

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

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

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

image

این به شما این امکان را می‌دهد که چندین متغیر وضعیت را، حتی از چندین کامپوننت، بدون ایجاد رندر مجدد بیش از حد، به‌روزرسانی کنید. اما این به این معنی است که UI تا بعد از تمام شدن تابع رویداد شما، و هر کدی که در آن وجود دارد، به‌روزرسانی نخواهد شد. این رفتار که به عنوان دسته‌بندی (Batching) نیز شناخته می‌شود، باعث می‌شود که برنامه React شما بسیار سریع‌تر اجرا شود. همچنین از روبه‌رو شدن با رندرهای "نیمه‌تمام" گیج‌کننده که فقط برخی از متغیرها به‌روزرسانی شده‌اند، جلوگیری می‌کند.

React بین -- هر کلیک به‌طور جداگانه مدیریت می‌شود. React فقط زمانی دسته‌بندی می‌کند که دسته بندی، ایمن باشد. این اطمینان می‌دهد که به عنوان مثال، اگر در کلیک اول، دکمه یک فرم را غیرفعال کند، کلیک دوم آن باعث ارسال دوباره اطلاعات نخواهد نشد.

به‌روزرسانی چندین باره یک وضعیت قبل از رندر بعدی

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

اکنون سعی کنید شمارشگر را افزایش دهید:

مشاهده و اجرای کد در CodeSandbox

      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 به عنوان تابع به‌روزرسانی شناخته می‌شود. وقتی شما آن را به یک تنظیم‌کننده وضعیت ارسال می‌کنید:

  • React این تابع را به صف اضافه می‌کند تا پس از اجرای تمام دیگر کدها در تابع رویداد پردازش شود.
  • در رندر بعدی، React از به سراغ صف می‌رود و وضعیت به‌روزرسانی نهایی را به شما می‌دهد.
      setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
    

این گونه است که React این خطوط کد را در حین اجرای تابع رویداد پردازش می‌کند:

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

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

به‌روزرسانی در صفnچیزی که برمی‌گرداند
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

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

به این دلیل است که کلیک بر روی دکمه "+3" در مثال بالا، وضعیت را سه واحد افزایش می‌دهد.

اگر پس از جایگزینی وضعیت آن را به‌روزرسانی کنید چه اتفاقی می افتد؟

در تابع رویداد زیر چه اتفاقی می افتد؟ فکر می‌کنید مقدار number در رندر بعدی چه خواهد بود؟

      <button onClick={() => {
  setNumber(number + 5);
  setNumber(n => n + 1);
}}>
    

مشاهده کد و نتیجه اجرای آن در CodeSandbox

      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>
    </>
  )
}
    

کاری که تابع رویداد می کند به شرح زیر است:

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

در رندر بعدی، React به سراغ صف وضعیت می‌رود:

به‌روزرسانی در صفnبرمی‌گرداند
"جایگزین با 5"0 (استفاده نشده)5
n => n + 155 + 1 = 6

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

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

اگر وضعیت را پس از به‌روزرسانی آن جایگزین کنید چه اتفاقی می‌افتد؟

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

      <button onClick={() => {
  setNumber(number + 5);
  setNumber(n => n + 1);
  setNumber(42);
}}>
    

مشاهده کد و نتیجه اجرای آن در CodeSandbox

      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>
    </>
  )
}
    

ری اکت کدهای بالا را در حین اجرای تابع رویداد به صورت زیر پردازش می‌کند:

  • setNumber(number + 5): مقدار number برابر 0 است، بنابراین setNumber(0 + 5) اجرا می‌شود. React "جایگزینی با را به صف خود اضافه می‌کند.
  • setNumber(n => n + 1): دستور n => n + 1 یک تابع به‌روزرسانی است. React آن تابع را به صف خود اضافه می‌کند.
  • setNumber(42): React "جایگزینی با را به صف خود اضافه می‌کند.

در رندر بعدی، React به سراغ صف وضعیت می‌رود:

به‌روزرسانی در صفnبرمی‌گرداند
"جایگزینی با 5"0 (استفاده نشده)5
n => n + 155 + 1 = 6
"جایگزینی با 42"6 (استفاده نشده)42

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

برای خلاصه کردن، این گونه می‌توانید کار setNumber را در نظر بگیرید:

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

پس از اتمام تابع رویداد، React یک رندر مجدد را فعال می‌کند. در حین رندر مجدد، React صف را پردازش می‌کند. توابع به‌روزرسانی در حین رندر اجرا می‌شوند، بنابراین توابع به‌روزرسانی باید باشند و فقط نتیجه را بازگردانی کنند. سعی نکنید حالت را داخل آنها تنظیم کنید یا دیگر اثرات جانبی را اجرا کنید. در حالت سختگیرانه، React هر تابع به‌روزرسانی را دو بار اجرا می‌کند (اما نتیجه دوم را نادیده می‌گیرد) تا به شما کمک کند تا اشتباهات را پیدا کنید.

روش نام‌گذاری متغیرها

نام گذاری آرگومان تابع به‌روزرسانی، قاعدتا براساس حروف اول متغیر وضعیت مربوطه انجام می‌شود:

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

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

جمع بندی

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

چالش ها

1. اصلاح شمارنده درخواست‌ها

شما روی یک برنامه بازار هنری کار می‌کنید که به کاربر اجازه می‌دهد چندین سفارش برای یک کالای هنری را همزمان ارسال کند. هر بار که کاربر دکمه "خرید" را فشار می‌دهد، شمارنده "در حال انتظار" باید یک واحد افزایش یابد. پس از سه ثانیه، شمارنده "در حال انتظار" باید کاهش یابد و شمارنده "تکمیل شده" باید افزایش یابد.

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

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

مشاهده و اصلاح کد در CodeSandbox

      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);
  });
}
    

2. پیاده‌سازی صف وضعیت خودتان

در این چالش، شما یک قسمت کوچک از React را از ابتدا پیاده‌سازی خواهید کرد! این چالش به اندازه‌ای که به نظر می‌رسد سخت نیست.

در پیش‌نمایش sandbox اسکرول کنید. متوجه خواهید شد که چهار مورد آزمایشی نشان داده شده می شود که به مثال‌هایی که قبلاً در این صفحه دیده‌اید مربوط می‌شوند. وظیفه شما این است که تابع getFinalState را پیاده‌سازی کنید تا نتیجه صحیح را برای هر یک از این موارد برگرداند. اگر آن را به درستی پیاده‌سازی کنید، تمام چهار تست باید موفق شوند.

شما دو آرگومان خواهید داشت: baseState وضعیت اولیه (مانند 0)، و queue یک آرایه است که شامل ترکیبی از اعداد (مانند 5) و توابع به‌روزرسانی (مانند n => n + 1) به ترتیب اضافه‌شده است.

وظیفه شما این است که وضعیت نهایی را برگردانید، درست مانند جداولی که در این صفحه نشان داده شده‌اند!

مشاهده و حل این چالش در CodeSandbox

      import { getFinalState } from './processQueue.js';
function increment(n) {
  return n + 1;
}
increment.toString = () => 'n => n+1';
export default function App() {
  return (
    <>
      <TestCase
        baseState={0}
        queue={[1, 1, 1]}
        expected={1}
      />
      <hr />
      <TestCase
        baseState={0}
        queue={[
          increment,
          increment,
          increment
        ]}
        expected={3}
      />
      <hr />
      <TestCase
        baseState={0}
        queue={[
          5,
          increment,
        ]}
        expected={6}
      />
      <hr />
      <TestCase
        baseState={0}
        queue={[
          5,
          increment,
          42,
        ]}
        expected={42}
      />
    </>
  );
}
function TestCase({
  baseState,
  queue,
  expected
}) {
  const actual = getFinalState(baseState, queue);
  return (
    <>
      <p>Base state: <b>{baseState}</b></p>
      <p>Queue: <b>[{queue.join(', ')}]</b></p>
      <p>Expected result: <b>{expected}</b></p>
      <p style={{
        color: actual === expected ?
          'green' :
          'red'
      }}>
        Your result: <b>{actual}</b>
        {' '}
        ({actual === expected ?
          'correct' :
          'wrong'
        })
      </p>
    </>
  );
}
    

اگر احساس می‌کنید که به بن‌بست رسیده‌اید، با این ساختار کد شروع کنید:

      export function getFinalState(baseState, queue) {
  let finalState = baseState;

  for (let update of queue) {
    if (typeof update === 'function') {
      // TODO: apply the updater function
    } else {
      // TODO: replace the state
    }
  }

  return finalState;
}
    

خطوط گمشده را پر کنید!

جواب چالش ها

چالش اول

داخل تابع رویداد handleClick، مقادیر pending و completed مطابق با آنچه که در زمان رویداد کلیک بوده‌اند هستند. برای اولین رندر، pending برابر 0 بود، بنابراین setPending(pending - 1) به setPending(-1) تبدیل می‌شود، که اشتباه است. از آنجا که شما می‌خواهید شمارنده‌ها را افزایش یا کاهش دهید، به جای تنظیم آنها به یک مقدار مشخص که در زمان کلیک تعیین شده است، می‌توانید توابع به‌روزرسانی را ارسال کنید:

مشاهده راه حل در CodeSandbox

      import { useState } from 'react';
export default function RequestTracker() {
  const [pending, setPending] = useState(0);
  const [completed, setCompleted] = useState(0);
  async function handleClick() {
    setPending(p => p + 1);
    await delay(3000);
    setPending(p => p - 1);
    setCompleted(c => c + 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);
  });
}
    

این اطمینان می‌دهد که وقتی شمارنده‌ای را افزایش یا کاهش می‌دهید، این کار را نسبت به جدیدترین وضعیت آن انجام می‌دهید به جای اینکه وضعیت در زمان کلیک چه بوده است.

چالش دوم

این دقیقاً الگوریتمی است که React برای محاسبه وضعیت نهایی استفاده می‌کند:

مشاهده کد و نتیجه اجرا در CodeSandbox

      export function getFinalState(baseState, queue) {
  let finalState = baseState;
  for (let update of queue) {
    if (typeof update === 'function') {
      // Apply the updater function.
      finalState = update(finalState);
    } else {
      // Replace the next state.
      finalState = update;
    }
  }
  return finalState;
}
    

حال شما می‌دانید که این بخش از React چگونه کار می‌کند!

رای
0
ارسال نظر
مرتب سازی:
اولین نفری باشید که نظر می دهید!