وقتی اپلیکیشن‌های React را توسعه می‌دهیم، ممکن است متوجه شده باشیم که به‌روزرسانی state، مقادیر جدید را بلافاصله بعد از تغییر نشان نمی‌دهد. دلیل این اتفاق این است که آپدیت state به صورت asynchronous انجام می‌شود. به این معنی که، React منتظر می‌ماند تا تمام کدهای درون event handler اجرا شوند و پس از آن state را آپدیت کند. به عنوان مثال:

const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(1);
    console.log(count);
  }, []);

کد بالا عدد ۰ را در کنسول چاپ می‌کند. این اتفاق به این دلیل رخ می‌دهد که React قبل از آپدیت state، ابتدا کدها را اجرا می‌کند. بنابراین، مقدار count در اولین رندر برابر با ۰ است.

این فرایند که به آن batching یا گروه‌بندی گفته می‌شود، توسط React برای پردازش آپدیت‌های متعدد stateها، بدون فراخوانی بیش از حد re-renderها مورد استفاده قرار می‌گیرد. این کار به React این امکان را می‌دهد که چندین state را به صورت همزمان به‌روزرسانی کند و در نتیجه، رندرهای غیرضروری کاهش یابد و کارایی و عملکرد کلی بهینه شود.

برای رفع مشکل آپدیت state در کد بالا، count را به عنوان dependency به هوک useEffect اضافه می‌کنیم:

useEffect(() => {
    setCount(1);
    console.log(count);
  }, [count]);

این بار خروجی دو بار در کنسول چاپ خواهد شد. بار اول که React از بلاک کد عبور می‌کند، مقدار count برابر با ۰ است. اما چون state مربوط به count تغییر کرده است، React صفحه را مجددا رندر کرده و مقدار count به ۱ تغییر پیدا می‌کند.

ما در این مقاله قصد داریم تا دلایلی که باعث می‌شود React بلافاصله state را آپدیت نکند، بررسی کنیم. همچنین مثالی ارائه کرده و توضیح می‌دهیم که در صورت نیاز به ایجاد تغییر جدید در state، هم در کلاس کامپوننت‌ها و هم در فانکشنال کامپوننت‌ها چه کارهایی باید انجام دهیم. در نهایت، نحوه مدیریت تنظیم مجدد state در کامپوننت‌های child را بررسی خواهیم کرد.

درک به‌روزرسانی‌های غیرهمزمان state در React

همانطور که بالاتر دیدیم، به‌روزرسانی‌های state به طور ذاتی فرآیندهای asynchronous هستند. زمانی که تابع setState را فراخوانی می‌کنیم، React به جای اعمال فوری تغییرات، آپدیت‌ها را زمان‌بندی می‌کند. هنگامی که setState را فراخوانی می‌کنیم، React صفی از آپدیت‌های معلق را نگه می‌دارد. سپس، چندین فراخوانی setState که در همان بلاک به صورت همزمان رخ می‌دهند، گروه‌بندی می‌کند.

این فرآیند گروه‌بندی برای جلوگیری از رندرهای غیرضروری و بهینه‌سازی عملکرد بسیار مهم است. React حداقل تغییرات مورد نیاز برای آپدیت state کامپوننت را محاسبه کرده و آن‌ها را در یک چرخه رندرینگ کارآمد اعمال می‌کند.

React چگونه آپدیت‌های state را انجام می‌دهد؟

برای آپدیت state در کامپوننت‌های React، ما از تابع this.setState یا تابع به‌روزرسانی که توسط هوک React.useState() return می‌شود، به ترتیب در کلاس کامپوننت‌ها و فانکشنال کامپوننت‌ها استفاده می‌کنیم.

آپدیت‌های state در React به صورت asynchronous هستند؛ زمانی که یک به‌روزرسانی درخواست می‌شود، هیچ تضمینی وجود ندارد که آپدیت‌ها بلافاصله انجام شوند. توابع به‌روزرسانی، تغییرات state کامپوننت را در یک صف قرار می‌دهند؛ اما React ممکن است تغییرات را به تعویق انداخته و چندین کامپوننت را در یک چرخه آپدیت نماید. به عنوان مثال:

const [count, setCount] = useState(0);

const handleClick = () => {
   setCount(1);
   setCount(1);
   setCount(1);
 };

در داخل تابع handleClick سه تابع به‌روزرسانی setCount داریم. با این حال، زمانی که event handler handleClick فراخوانی می‌شود، state count به ۱ آپدیت می‌گردد.

برای درک این موضوع، یک console.log به کد خود اضافه می‌کنیم تا state count را پس از هر تابع به‌روزرسانی در کنسول ثبت نماییم.

const handleClick = () => {
   setCount(1);
   console.log("log 1:", count);
   setCount(1);
   console.log("log 2:", count);
   setCount(1);
   console.log("log 3:", count);
 };

متوجه خواهیم شد که کنسول همچنان مقدار قدیمی count (۰) را ثبت می‌کند. این به این دلیل است که در اولین re-render، مقدار count برابر ۰ است و state تنها پس از تکمیل فرآیند batch آپدیت می‌شود.

به‌طور مشابه، کد زیر را در نظر می‌گیریم:

const handleClick = () => {
      setName("Amaka")
      setAge(20)
      setAddress("No 3 Rodeo drive")
}

در این قطعه کد، سه فراخوانی مختلف برای آپدیت و re-render کردن کامپوننت وجود دارد. فراخوانی توابع به‌روزرسانی یکی پس از دیگری و re-render کردن کامپوننت‌های parent و child پس از هر فراخوانی، در بیشتر موارد ناکارآمد خواهد بود. به همین دلیل ری‌اکت stateها را به‌صورت batch آپدیت می‌کند.

فرقی نمی‌کند که چند فراخوانی setState() در یک event handler مانند handleClick وجود داشته باشد؛ در نهایت، همه‌ی آن‌ها فقط یک re-render در پایان event ایجاد می‌کنند. این فرآیند برای حفظ عملکرد مطلوب در برنامه‌های بزرگ بسیار حیاتی است. ترتیب درخواست‌های به‌روزرسانی همیشه رعایت می‌شود؛ یعنی React همیشه ابتدا به درخواست‌های اولیه رسیدگی می‌کند.

ما متوجه شدیم که تأخیر در پردازش درخواست‌های آپدیت برای batching آن‌ها مفید است. در ادامه، مدیریت شرایطی را بررسی می‌کنیم که در آن نیاز داریم تا قبل از استفاده از مقادیر جدید، منتظر تکمیل این به‌روزرسانی‌ها باشیم.

انجام عملیات در کلاس کامپوننت‌ها

setState() callback

پارامتر دوم متد setState() یک تابع callback اختیاری است. این تابع زمانی اجرا می‌شود که عملیات setState() کامل شده و کامپوننت re-render شده باشد. این تابع callback تضمین می‌کند که بعد از اعمال به‌روزرسانی state اجرا شود:

handleSearch  = (e) => {
    this.setState({
    searchTerm: e.target.value
  },() => {
    // Do an API call with this.state.searchTerm
  });
}

componentDidUpdate

تابع componentDidUpdate بلافاصله پس از آپدیت state اجرا می‌شود. برای جلوگیری از ایجاد یک حلقه بی‌نهایت، همیشه باید از یک شرط استفاده کنیم تا مطمئن شویم که state قبلی و state فعلی یکسان نیستند:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    // Do something here
  }
}

مدیریت به‌روزرسانی state در کامپوننت‌های فانکشنال

هوک useEffect()

همان‌طور که در بخش مقدمه دیدیم، می‌توانیم در هنگام آپدیت state از هوک useEffect برای اعمال side effectها استفاده کنیم. متغیر state می‌تواند به‌عنوان یک dependency در این هوک اضافه شود، به‌طوری که با تغییر مقدار state، این هوک اجرا شود. می‌توانیم از‌ هوک useEffect برای پی بردن به تغییرات state استفاده کنیم:

import React,{useState, useEffect} from 'react';

const App = () => {
  const [count, setCount] = useState(1);

  useEffect(() => {
    if (count > 5) {
      console.log('Count is more that 5');
    } else {
      console.log('Count is less that 5');
    }
  }, [count]);

  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>{count}</p>

      <button onClick={handleClick}>
        add
      </button>
    </div>
  );
};

export default App;

تابع callback در هوک useEffect تنها زمانی اجرا می‌شود که یکی از متغیرهای state که در آرایه dependency آن ذکر شده است، تغییر کند.

باید به این نکته توجه داشته باشیم که از استفاده بی‌مورد از هوک useEffect خودداری کنیم. زیرا، هوک useEffect برای هماهنگ‌سازی کامپوننت‌ها با سیستم‌های خارجی، مانند زمانی که network و API call انجام می‌دهیم طراحی شده است. برای اطلاعات بیشتر مطالعه مستندات رسمی می‌تواند مفید باشد.

useState() callback

هوک useState در React مانند setState تابع callback داخلی ندارد. با این حال، می‌توانیم از آن برای دستیابی به عملکردی مشابه عملکرد هوک useEffect استفاده کنیم، که به ما این امکان را می‌دهد تا پس از رندر شدن کامپوننت، side effectها را اجرا نماییم. کد قبلی که داشتیم به صورت زیر بود:

const [count, setCount] = useState(0);

 const handleClick = () => {
    setCount(1);
    setCount(1);
    setCount(1);
  };

برای آپدیت state، از یک تابع به‌روزرسانی استفاده می‌کنیم، چیزی شبیه به این تابع: n => n + 1، که در آن n مقدار state قدیمی یا قبلی است. اکنون کدی که داریم به شکل زیر می‌باشد:

const handleClick = () => {
    setCount((n) => n + 1);
    setCount((n) => n + 1);
    setCount((n) => n + 1);
  };

زمانی که تابع handleClick فراخوانی می‌شود، مقدار count به ۳ تغییر می‌کند. این به این دلیل است که وقتی React از میان بلاک‌های کد عبور می‌کند، state count تغییر می‌کند.

اکنون این موضوع را بیشتر بررسی می‌کنیم. در اولین تابع setCount:

  1. مقدار n برابر ۰ است، پس ۰ + ۱ = ۱ (React این را دسته‌بندی می‌کند و مقدار count را به ۱ تغییر می‌دهد)
  2. اکنون مقدار n برابر ۱ است، پس ۱ + ۱ = ۲ (React می‌بیند که مقدار count تغییر کرده است. بنابراین، مقدار جدید count را به مقدار قبلی اضافه کرده و دسته‌بندی می‌کند)
  3. حال مقدار n برابر ۲ است، پس ۲ + ۱ = ۳ (React دوباره می‌بیند که مقدار count تغییر پیدا کرده است. پس مقدار جدید count را به مقدار قبلی اضافه کرده و دسته‌بندی می‌کند)

در نهایت، فرآیند دسته‌بندی کامل می‌شود و React مقدار جدید count را برابر ۳ تنظیم می‌نماید.

ما می‌توانیم هر نام دلخواهی را به تابع آپدیت‌کننده اختصاص دهیم. در این مقاله برای این که مفهوم به راحتی قابل انتقال باشد از prevState استفاده می‌کنیم. بنابراین داریم: setCount((prevState) => prevState + 1).

مدیریت ریست‌های state در کامپوننت‌های child

به طور پیش‌فرض، تا زمانی که کامپوننت در حال رندر شدن باشد یا در همان موقعیت ثابت باقی بماند، state حفظ می‌شود. به عنوان مثال:

import { useState } from "react";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <User />
      <User />
    </div>
  );
}

export function User() {
  const [age, setAge] = useState(0);
  const handleAddUser = () => {
    setAge((prevState) => prevState + 1);
  };
  return (
    <div>
      <h3>Age: {age}</h3>
      <button type="button" onClick={handleAddUser}>
        Add user
      </button>
    </div>
  );
}

ما دو کامپوننت child در App داریم. اگر سعی کنیم بر روی دکمه‌های مختلف کلیک کنیم خواهیم دید که هر دو child به طور همزمان آپدیت نمی‌شوند. این به این دلیل است که آن‌ها به عنوان دو کامپوننت مختلف با stateهای مختلف در نظر گرفته می‌شوند؛ و ما می‌دانیم تا زمانی که کامپوننت‌ها در حال رندر شدن باشند یا در همان موقعیت باقی بمانند، state آن‌ها حفظ می‌شود. به عنوان مثال:

import { useState } from "react";
import "./styles.css";
export default function App() {
  const [show, setShow] = useState(true);
  const handleAddUser = () => {
    setShow(!show);
  };
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <button type="button" onClick={handleAddUser}>
        Toggle
      </button>
      {show ? <User name="Chimezie" /> : <User name="Innocent" />}
    </div>
  );
}
export function User({ name }) {
  const [age, setAge] = useState(0);
  const handleAddUser = () => {
    setAge((prevState) => prevState + 1);
  };
  return (
    <div>
      <h3>
        Name: {name}, Age: {age}
      </h3>
      <button type="button" onClick={handleAddUser}>
        Add user
      </button>
    </div>
  );
}

در حال حاضر ما از یک شرط برای رندر کردن هر دو کامپوننت child استفاده می‌کنیم. با این حال، نام‌هایی داریم که برای تمایز بین زمانی که هر کامپوننت نمایش داده می‌شود یا رندر می‌شود، استفاده می‌کنیم. بر روی دکمه‌ها کلیک می‌کنیم و بین هر دو کامپوننت جابه‌جا می‌شویم. مشاهده خواهیم کرد تا زمانی که کامپوننت در حال رندر شدن باشد و در همان موقعیت باقی بماند، state حفظ می‌شود.

برای ریست کردن state در کامپوننت‌های child، روش‌های مختلفی وجود دارد که می‌توانیم از آن‌ها استفاده کنیم.

استفاده از موقعیتهای مختلف

در مثال شرطی بالا، ما هر دو کامپوننت را در همان موقعیت رندر می‌کنیم و به همین دلیل آن‌ها از یک state مشترک استفاده می‌کنند و این state با وجود تغییر، حفظ می‌شود.

اما اگر آن‌ها را به طور جداگانه رندر کنیم، هر بار که تغییر وضعیت می‌دهیم، state ریست می‌شود:

return (
    <div className="App">
      <button type="button" onClick={handleAddUser}>
        Toggle
      </button>

      {show && <User name="Chimezie" />}
      {!show && <User name="Innocent" />}
    </div>
  );

این اتفاق به این دلیل رخ می‌دهد که هر دو کامپوننت child در یک موقعیت رندر یکسان قرار ندارند. بنابراین زمانی که عدد age را افزایش می‌دهیم و سپس به کامپوننت child بعدی تغییر وضعیت می‌دهیم، عدد age به طور خودکار ریست می‌شود.

Key props

راه دیگری که برای ریست کردن state در کامپوننت‌های child وجود دارد، استفاده از props key است. معمولاً از props key برای پیمایش در لیست‌ها و منحصربه‌فرد کردن هر آیتم استفاده می‌کنیم. به طور مشابه، می‌توانیم از آن برای ریست کردن state کامپوننت child نیز بهره‌مند شویم. زمانی که props key یک کامپوننت child تغییر می‌کند، React آن را به عنوان یک کامپوننت جدید در نظر می‌گیرد و بنابراین، state آن ریست می‌شود. با استفاده از مثال قبلی:

return (
  <div className="App">
    <button type="button" onClick={handleAddUser}>
      Toggle
    </button>

    {show ? (
      <User key="Chimezie" name="Chimezie" />
    ) : (
      <User key="Innocent" name="Innocent" />
    )}
  </div>
);

در این مثال، ما props key منحصربه‌فردی به هر دو کامپوننت child اختصاص می‌دهیم. با تغییر props key در هر بار تغییر، state نیز با آن ریست می‌شود.

جمع‌بندی

زمانی که در ری‌اکت state را مدیریت می‌کنیم، درک ماهیت asynchronous آن و تکنیک batching برای بهینه‌سازی عملکرد اهمیت زیادی دارد. ری‌اکت stateها را به‌صورت دسته‌ای آپدیت می‌کند تا از re-renderهای غیرضروری جلوگیری کند.

برای جلوگیری از مشکلاتی که در آن React پس از تغییر state، کامپوننت‌ها را re-render نمی‌کند، بهتر است از تغییر مستقیم آبجکت‌ها در state پرهیز کرده و از توابع setter مانند setState یا setCount استفاده کنیم. تکنیک‌هایی مانند استفاده از props key برای کامپوننت‌های child یا مدیریت dependencyها در هوک useEffect نیز می‌تواند چالش‌ها در هماهنگی state کامپوننت‌های child و parent را برطرف کند.

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