logo
داکیومنت ریکت - قسمت پنجم
ریکت

ریکت

29 مرداد 1402

سلام دوستان امیدوارم خوب باشید و همیشه درحال یادگیری🧑‍💻. توی این قسمت از داکیومنت ریکت قرار هست، این موضوعات رو بررسی کنیم:

  • ایونت ها و Event Propagation
  • استیت ها و مزیتش نسبت به متغیر های محلی چیه؟
  • مراحل رندر شدن یک کامپوننت

ایونت ها

Event handlers تابع هایی هستند که در زمان اتفاق افتادن تعامل مثل کلیک، هاور و... اجرا می‌شوند.

مثلا فرض کنید ما دکمه ایی داریم بصورت زیر:

export default function Button() {
  return (
    <button>
      I don't do anything
    </button>
  );
}

می‌خوایم زمانی که روی دکمه کلیک انجام میشه، پیام به کاربر نشون بده. ابتدا لازم هست اون تابع ایی که وظیفه مدیریت کردن این ایونت رو داره تعریف کنیم. بعد از تعریف تابع handleClick میایم اون رو به پراپ (prop) onClick دکمه پاس میدیم.

export default function Button() {
  function handleClick() {
    alert('You clicked me!');
  }

  return (
    <button onClick={handleClick}>
      Click me
    </button>
  );
}

تابع handleClick یک event handler هستش که:

  • معمولا داخل کامپوننت فعلی تعریف میشه
  • بهتر هست با handle شروع بشه و در ادامه اسم اون ایونت بیاد

یک نکته مهم، تابع ایی که داریم به onClick میدیم، نباید فراخوانی بشه و صرفا باید پاس بدیم.

passing a function (correct)calling a function (incorrect)
<button onClick={handleClick}><button onClick={handleClick()}>

تفاوت فراخوانی با پاس دادن اینجاست که، زمانی که تابع handleClick رو پاس میدیم هر زمان که ایونت اتفاق بیوفته صدا زده میشه، اما فراخوانی در زمان رندر شدن اون تابع صدا زده میشه.

Event Propagation

یک بحث خیلی مهم داخل ایونت ها داریم به نام Event propagation یا گسترش ایونت. حالا این یعنی چی؟

فرض کنید در همین مثال پایین، ما رو دکمه Play Movie کلیک کنیم چه اتفاقی میوفته؟ اول پیام !Playing رو داریم، بعدش هم این !You clicked on the toolbar پیام رو نشون میده. در حالی که ما روی دکمه Play Movie کلیک کردیم داریم دو تا alert می‌بینیم.

علتش همین Event propagation هستش. درواقع وقتی روی فرزند کلیک می‌کنیم این ایونت ما گسترش پیدا میکنه و به سمت بالا درخت میره که والد ما باشه و اون ایونت هم trigger میشه و alert دوم هم نشون داده میشه.

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <button onClick={() => alert('Playing!')}>
        Play Movie
      </button>
      <button onClick={() => alert('Uploading!')}>
        Upload Image
      </button>
    </div>
  );
}

این Event propagation روی همه ایونت ها اتفاق میوفته بجز onScroll.

جلوگیری از گسترش ایونت

برای اینکه جلو گسترش ایونت رو بگیریم، توی onClick فرزند باید از ()e.stopPropagation استفاده کنیم.

function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <Button onClick={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onClick={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}

در شرایط خاص اگر لازم باشه که propagation رو داشته باشیم حتی اگر جلو اون رو گرفته باشیم، باید از onClickCapture داخل والد استفاده کنیم. به اینصورت عمل میکنه که ابتدا فانکشن ایونت والد اجرا میشه بعدش میاد فانکشن ایونت ایی که فرزند ما هست و روش کلیک شده اجرا میشه.

یعنی ما در مثالی که داشتیم اگه روی دکمه Play Movie کلیک کنیم ابتدا !You clicked on the toolbar نشون داده میشه و بعدش !Playing.

export default function Toolbar() {
  return (
    <div className="Toolbar" onClickCapture={() => {
      alert('You clicked on the toolbar!');
    }}>
      <Button onClick={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onClick={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}

پیشگیری از رفتار پیشفرض

بعضی از مرورگر ها رفتار های پیشفرضی دارن. بطور مثال برای تگ Form زمانی که روی یک دکمه کلیک بشه، فرم submit میشه و کل صفحه reload میشه.

برای اینکه جلو این reload رو بگیریم از ()e.preventDefault استفاده می‌کنیم.

export default function Signup() {
  return (
    <form onSubmit={e => {
      e.preventDefault();
      alert('Submitting!');
    }}>
      <input />
      <button>Send</button>
    </form>
  );
}

بطور کلی ()e.preventDefault جلو رفتار های پیشفرض مرورگر رو میگیره. -- بعضی از ایونت ها این رفتار های پیشفرض رو دارند که می‌تونیم جلوش رو بگیریم.

استیت: مموری کامپوننت ها

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

حالا فرض کنید ما بجای استیت از متغیر معمولی استفاده کردیم، چه اتفاقی میوفته؟

توی مثال زیر با هربار کلیک روی دکمه "next" قرار هست مقدار index یکی یکی افزایش پیدا کنه و تصویر بعدی نشون داده بشه. اما کار نمی‌کنه!



تابع handleClick داره مقدار index رو افزایش میده، اما دو تا مشکل هست:

  1. برای متغیر های محلی (local) بین رندر ها، اطلاعاتی که دارند حفظ نمی‌شوند و از اول مقداردهی می‌شوند.
  2. تغییر مقدار متغیر های محلی باعث ایجاد رندر مجدد نمی‌شوند.

برای اینکه کامپوننت با دیتا جدید آپدیت بشه لازم هست دو اتفاق بیوفته:

  1. دیتا بین رندرها حفظ بشه
  2. دیتا جدید باعث ایجاد رندر مجدد کامپوننت بشه

که هوک useState هر دو شرط رو برامون فراهم کرده.

ساختار useState

وقتی که useState رو صدا میزنیم، داریم به ریکت میگیم که کامپوننت ما باید یک چیزی رو یادش بمونه.

const [index, setIndex] = useState(0);

هوک useState همیشه آرایه دو عضوی بر میگردونه که عضو اول مقدار state و دومی هم فانکشن setter اون هست. تنها آرگومانی که useState داره مقدار اولیه هست. که در مثال بالا مقدار اولیه index صفر قرار گرفته.

اما state ایی که تعریف کردیم در عمل این اتفاق براش میوفته:

  1. کامپوننت رندر میشه، چون مقدار اولیه 0 هست پس این [0, setIndex]رو برمیگردونه، و یادش میمونه که 0 آخرین مقدار هست.
  2. استیت رو آپدیت می‌کنیم. مثلا با کلیک روی یک دکمه setIndex(index + 1) فراخوانی میشه. چون مقدار قبلی index صفر بوده پس مقدار جدید 1 میشه و دستور برای رندر مجدد رو میده.
  3. داخل رندر مجدد ریکت میبینه useState(0) هست، اما چون یادش مونده که index را ما 1 کردیم پس بجاش [1, setIndex] رو برمیگردونه.



وقتی داخل کامپوننت میایید useState رو فراخوانی می‌کنید هیچ اطلاعاتی درباره اینکه به کدوم استیت دارید اشاره می‌کنید نمیدید. یک مشخص کننده به useState پاس نمیدید که بدونید کدوم استیت رو میخواید، پس موقع return از کجا میفهمه کدوم استیت رو باید برگدونه؟

در واقع ریکت برای هر کامپوننت یک آرایه ایی تشکیل میده از استیت هایی که داریم و برای هر استیت ایی که تعریف میشه یک آرایه تشکیل میشه که شامل خود state و تابع setter اون هست یعنی به صورت زیر:

pair = [initialState, setState]

بعدش این آرایه رو داخل index فعلی اون آرایه اصلی ذخیره می‌کنیم، و اون index رو یک واحد بهش اضافه می‌کنیم تا برای اضافه کردن استیت جدید داخل آرایه آماده باشیم.

componentHooks[currentHookIndex] = pair

زمانی که از تابع setter استیت استفاده می‌کنیم مقدار index فعلی صفر میشه و بعدش رندر اتفاق میوفته. یک مثال ساده شده useState رو اینجا ببینید:

let componentHooks = [];
let currentHookIndex = 0;

// How useState works inside React (simplified).
function useState(initialState) {
  let pair = componentHooks[currentHookIndex];
  if (pair) {
    // This is not the first render,
    // so the state pair already exists.
    // Return it and prepare for next Hook call.
    currentHookIndex++;
    return pair;
  }

  // This is the first time we're rendering,
  // so create a state pair and store it.
  pair = [initialState, setState];

  function setState(nextState) {
    // When the user requests a state change,
    // put the new value into the pair.
    pair[0] = nextState;
    updateDOM();
  }

  // Store the pair for future renders
  // and prepare for the next Hook call.
  componentHooks[currentHookIndex] = pair;
  currentHookIndex++;
  return pair;
}

به عنوان منبع تکمیلی این مقاله رو پیشنهاد می‌کنم بخونید.

استیت ها ایزوله هستند

اگر یک کامپوننت را دو بار فراخوانی کنیم استیت های ایزوله هستند و روی همدیگه اثری ندارند. علتش رو هم بالا گفتیم چون آرایه ایی که میاد استیت های این دو رو ذخیره میکنه از همدیگه مجزا هستش بخاطر همین روی همدیگه اثری ندارن.



رندر شدن کامپوننت

قبل از اینکه کامپوننت ها روی صفحه نمایش داده بشه، لازم هست توسط ریکت رندر بشه. درک این مراحل خیلی کمک کننده هست.

فرض کنید کامپوننت های شما در یک آشپزخونه درست میشن. داخل این سناریو ریکت نقش گارسون رو داره که درخواست ها رو از سمت مشتری به آشپزخونه میاره. این پروسه سه مرحله داره:

  1. Triggering - رندر کردن رو trigger کنیم (درخواست رو به آشپزخونه بیاریم)
  2. Rendering - رندر کردن کامپوننت (اون درخواست رو داخل آشپزخونه آماده کنیم)
  3. Committing - تغییرات رو به دام اعمال کنیم (درخواست رو روی میز مشتری قرار دهیم)


قدم 1: رندر کردن رو trigger کنیم

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

  1. رندر اولیه اون کامپوننت باشه.
  2. یک استیت داخل کامپوننت آپدیت بشه.

زمانی که اپ استارت میشه باید رندر اولیه رو trigger کنیم. این کار با فراخوانی createRoot انجام میشه و بعدش به کمک متد render میایم کامپوننت رو رندر می‌کنیم.

import Image from './Image.js';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'))
root.render(<Image />);

حالا اگه یک استیت آپدیت بشه نیاز به رندر مجدد داریم.

به اینصورت که ریکت وقتی متوجه آپدیت شدن یک استیت میشه اون مقدار جدید رو به آشپزخونه ما میبره و اونجا تحویل میده بعد اینکه تغییرات روی استیت انجام شد، دوباره تحویل ریکت میده و رندر رو انجام میده.



قدم 2: رندر کردن کامپوننت

رندر کردن یعنی صدا زدن کامپوننت هامون (“Rendering” is React calling your components). حالا اگه رندر اولیه باشه میاد اون کامپوننت روت (root) رو صدا میزنه، اما برای رندر های بعدی، میاد همون کامپوننت ایی که مثلا استیت اش آپدیت شده رو صدا میزنه.

این پروسه بازگشتی هست. اگه کامپوننت ایی که قرار هست آپدیت بشه بیاد یک سری کامپوننت دیگه برگردونه، ریکت اون کامپوننت ها رو اول رندر میکنه، و اگه اون کامپوننت هم باز داخلش کامپوننت دیگه با به همین صورت عمل میکنه و الی آخر. اینقدر این پروسه ادامه پیدا می‌کنه تا به آخر برسیم و کامپوننت تو در تو دیگه ایی نباشه.

در مثال پایین ریکت Gallery و Images رو چندین بار فراخوانی میکنه.

export default function Gallery() {
  return (
    <section>
      <h1>Inspiring Sculptures</h1>
      <Image />
      <Image />
      <Image />
    </section>
  );
}

function Image() {
  return (
    <img
      src="https://i.imgur.com/ZF6s192.jpg"
      alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
    />
  );
}

در فاز رندر اولیه: ریکت میاد DOM nodes برای section، h1 و سه تا img تشکیل میده.

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


قدم 3: تغییرات رو به دام اعمال کنیم

در فاز رندر اولیه: ریکت از ()appendChild که یک API دام هست استفاده می‌کنه برای اینکه تمام DOM nodes هایی که ایجاد شده رو نمایش بده. در فاز رندر مجدد: در این حالت فقط تغییرات ضروری که در مرحله دوم محاسبه شده را روی دام میایم اعمال می‌کنیم.

این نکته مهم هست که ریکت بین رندر مجدد فقط اون قسمت از DOM nodes ما تغییر کرده رو میاد آپدیت میکنه نه کل دام رو.

در مثال پایین با هر بار آپدیت شدن time ریکت فقط محتوا h1 رو میاد آپدیت می‌کنه، و به input که ما اینجا داریم کاری نداره.



و در نهایت داخل مرورگر می‌تونیم این تغییرات رو ببینیم.

داخل داکیومنت به این مرحله آخر Browser paint گفته که بنظرم خیلی قشنگه :)))




خوب بچه ها امیدوارم این قسمت براتون مفید بوده باشه. خوشحال میشم اگه انتقادی یا پیشنهادی دارید برام بنویسید تا محتوای بهتری تولید بشه ❤️

منابع:

این پست برات مفید بود؟

0

heart

0

like

0

happy

0

sad