فصل ضمیمه: بهینه‌سازی برنامه

Appendix: Application Optimization

26 مرداد 1399
فصل ضمیمه: بهینه سازی برنامه

تا این جلسه با hook های زیادی آشنا شدیم و فقط hook های شخصی سازی شده باقی مانده اند اما قبل از آنکه به آن ها برسیم باید برنامه را کمی بهینه سازی کنیم. useMemo یکی از hook هایی است که به ما اجازه می دهد جلوی render شدن های ناخواسته را بگیریم و در قسمت های قبل از آن استفاده کردیم اما می توانیم با ترکیب آن با سایر موارد مثل useCallback برنامه خود را بهینه سازی کنیم. اگر به فایل ingredients.js نگاه کنید 2 عدد reducer را می بینید و یکی از قسمت های جالب آن کد زیر است:

      <IngredientForm
        onAddIngredient={addIngredientHandler}
        loading={httpState.loading}
      />

ما در این قسمت addIngredientHandler را به IngredientForm پاس داده ایم. می دانیم که addIngredientHandler فقط یک تابع است بنابراین هر زمان که کامپوننت ما دوباره ساخته شود، addIngredientHandler نیز دوباره ساخته می شود چرا که درون کامپوننت قرار دارد. حواستان باشد که useReducer دوباره ساخته نمی شود چرا که تابع خاصی بوده و react متوجه می شود که قبلا اجرا شده و مقدار خاصی را دارد اما addIngredientHandler یک تابع عادی است بنابراین دوباره ساخته می شود. حالا ما یک تابع جدید را که تازه ساخته شده است به IngredientForm پاس می دهیم. از آنجایی که یک مقدار جدید و تغییر کرده به IngredientForm پاس داده می شود، این کامپوننت هم دوباره ساخته می شود.

شاید با خودتان بگویید که در IngredientForm از Memo استفاده کرده ایم بنابراین چرا باید دوباره ساخته شود؟

const IngredientForm = React.memo(props => {
// بقیه کدها //

بگذارید به شما ثابت کنم که این اتفاق می افتد. یک دستور log به اول این کامپوننت اضافه کنید:

const IngredientForm = React.memo(props => {
  const [enteredTitle, setEnteredTitle] = useState('');
  const [enteredAmount, setEnteredAmount] = useState('');
  console.log('RENDERING INGREDIENT FORM');
// بقیه کدها //

از نظر تئوری، این دستور log فقط زمانی باید اجرا شود که درون input های خودمان تایپ کنیم چرا که تنها مقادیر تغییر دهنده فرم ها همان input ها هستند. اما اگر در حال حاضر به مرورگر بروید و چیزی را تایپ کرده و دکمه Add Ingredient را بزنید، شاهد دوباره render شدن چند باره این کامپوننت هستید!

رندر شدن چندین باره ی کامپوننت
رندر شدن چندین باره کامپوننت

چرا؟ به دلیل اینکه با تغییر وضعیت loading یک بار و با هر item اضافه شده در UI (نمایش آیتم ها در ظاهر برنامه) یک بار دیگر بارگذاری و ساخته می شود. ما نمی توانیم در مورد loading کار بکنیم چرا که آن را درون خود فرم قرار داده ایم بنابراین دو بار render شدن طبیعی است اما سومین بار اضافی است. برای حل این مشکل می توانیم تابع addIngredientHandler را درون useCallback قرار دهیم:

  const removeIngredientHandler = useCallback(ingredientId => {
    dispatchHttp({ type: 'SEND' });
    fetch(
      `https://react-hooks-update.firebaseio.com/ingredients/${ingredientId}.json`,
      {
        method: 'DELETE'
      }
    )
      .then(response => {
        dispatchHttp({ type: 'RESPONSE' });
        // setUserIngredients(prevIngredients =>
        //   prevIngredients.filter(ingredient => ingredient.id !== ingredientId)
        // );
        dispatch({ type: 'DELETE', id: ingredientId });
      })
      .catch(error => {
        dispatchHttp({ type: 'ERROR', errorMessage: 'Something went wrong!' });
      });
  }, []);

  const clearError = useCallback(() => {
    dispatchHttp({ type: 'CLEAR' });
  }, []);

در جلسات قبل از useCallback استفاده کرده بودیم بنابراین با آن آشنا هستید. در قسمت [] در انتهای useCallback که مخصوص مشخص کردن وابستگی ها بود هیچ چیزی را وارد نکرده ام. می دانید چرا؟ وابستگی این تابع فقط dispatchHttp است و قبلا هم گفته بودم که نیازی به مشخص کردن آن نیست چرا که خود react آن را می شناسد و بین چرخه های render آن را تغییر نمی دهد. بنابراین می توانید آن را به عنوان وابستگی ذکر کنید اما مجبور نیستید. حالا می توانید دوباره کدها را در مرورگر تست کنید. این بار فقط دو بار render شدن را خواهید دید.

همین مسئله برای ingredientList نیز اتفاق می افتد:

<section>
    <Search onLoadIngredients={filteredIngredientsHandler} />
    <IngredientList
        ingredients={userIngredients}
        onRemoveItem={removeIngredientHandler}
    />
</section>

زمانی که ingredients یا محتویات تغییر می کند، IngredientList نیز دوباره ساخته می شود که مشکلی نیست اما removeIngredientHandler از useCallback استفاده نمی کند بنابراین دقیقا مانند مورد قبل باعث render های اضافی خواهد شد. من یک دستور log دیگر درون IngredientList قرار می دهم تا از این مسئله مطمئن شوم:

const IngredientList = props => {
  console.log('RENDERING INGREDIENTLIST');
  return (
    <section className="ingredient-list">
      <h2>Loaded Ingredients</h2>
      <ul>
        {props.ingredients.map(ig => (
// بقیه کدها //

حالا با تست کدها در مرورگر شاهد چنین صحنه ای خواهیم بود:

اثبات رندر های چند باره با console.log های مختلف
اثبات رندر های چند باره با console.log های مختلف

زمانی که آیتمی را حذف می کنیم باید دوباره Render شود اما نباید به خاطر نمایش یک علامت loading در بالای صفحه دوباره render شود. برای حل این مشکل ابتدا به فایل ingredients.js می رویم تا تابع removeIngredientHandler را درون یک callback قرار دهیم:

  const removeIngredientHandler = useCallback(ingredientId => {
    dispatchHttp({ type: 'SEND' });
    fetch(
      `https://react-hooks-update.firebaseio.com/ingredients/${ingredientId}.json`,
      {
        method: 'DELETE'
      }
    )
      .then(response => {
        dispatchHttp({ type: 'RESPONSE' });
        // setUserIngredients(prevIngredients =>
        //   prevIngredients.filter(ingredient => ingredient.id !== ingredientId)
        // );
        dispatch({ type: 'DELETE', id: ingredientId });
      })
      .catch(error => {
        dispatchHttp({ type: 'ERROR', errorMessage: 'Something went wrong!' });
      });
  }, []);

منطق نوشتاری این useCallback دقیقا مانند مورد قبلی است. در مرحله بعد به فایل ingredientList.js می رویم تا آن را درون react.memo قرار بدهیم. در غیر این صورت جلوی render های اضافی گرفته نمی شود:

const IngredientList = React.memo (props => {
  console.log('RENDERING INGREDIENTLIST');
  return (
    <section className="ingredient-list">
      <h2>Loaded Ingredients</h2>
      <ul>
        {props.ingredients.map(ig => (
          <li key={ig.id} onClick={props.onRemoveItem.bind(this, ig.id)}>
            <span>{ig.title}</span>
            <span>{ig.amount}x</span>
          </li>
        ))}
      </ul>
    </section>
  );
});

حالا اگر به مرورگر برویم و این کد را تست کنیم، شاهد یک بار re-render خواهیم بود نه چندین بار. این یک روش است اما برای اینکه به شما نشان بدهم روش های دیگری نیز وجود دارد، این کامپوننت را از react.memo خارج می کنم و به حالت قبل برمی گردم.

وارد فایل ingredients.js می شویم و یک hook دیگر به نام useMemo را وارد این فایل می کنم:

import React, { useReducer, useEffect, useCallback, useMemo } from 'react';

اگر یادتان باشد useCallback برای ذخیره توابعی بود که تغییر پیدا نمی کردند و useMemo برای ذخیره مقادیری است که تغییر نمی کنند. برای استفاده از آن ابتدا IngredientList را از جایی که هست (درون section در قسمت JSX) برمی داریم و درون یک متغیر قرار می دهیم:

  const ingredientList = useMemo(() => {
    return (
      <IngredientList
        ingredients={userIngredients}
        onRemoveItem={removeIngredientHandler}
      />
    );
  }, [userIngredients, removeIngredientHandler]);

در واقع useMemo یک تابع می گیرد که باید مقدار مورد نظر شما را return کند به همین دلیل است که آن را به شکل بالا نوشته ایم. دومین پارامتر useMemo مربوط به وابستگی های شما است. من userIngredients و removeIngredientHandler را به آن داده ام، یعنی این دو مورد از وابستگی های مربوط به  IngredientList است بنابراین زمانی که این دو تغییر کنند باید دوباره تابع بالا را اجرا کنی تا یک IngredientList جدید به دست بیاوریم.

حالا می توانیم ثابت ingredientList را درون section قرار بدهیم:

<section>
    <Search onLoadIngredients={filteredIngredientsHandler} />
    {ingredientList}
</section>

در نهایت تنها مورد باقی مانده ErrorModal است که در onClose یک تابع را صدا می زند:

  return (
    <div className="App">
      {httpState.error && (
        <ErrorModal onClose={clearError}>{httpState.error}</ErrorModal>
      )}
// بقیه کدها //

بنابراین clearError را نیز باید درون useCallback قرار بدهیم:

  const clearError = useCallback(() => {
    dispatchHttp({ type: 'CLEAR' });
  }, []);

خود ErrorModal نیز از react.memo استفاده می کند بنابراین این مشکل نیز حل می شود.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری دوره جامع آموزش ری اکت توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

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