فصل ضمیمه: آشنایی با hookهای شخصی‌سازی شده

Appendix: Familiarity with Personalized Hooks

22 بهمن 1399
فصل ضمیمه: آشنایی با hook های شخصی سازی شده

تقریبا به پایان این فصل رسیده ایم اما هنوز یک مسئله باقی مانده است. custom hooks یا hook هایی که خودتان آن ها را می سازید! بله شما می توانید hook هایتان را خودتان بسازید. من درون پوشه اصلی برنامه (src) یک پوشه دیگر به نام hooks می سازم. من می خواهم درون این پوشه یک custom hook به نام http داشته باشم بنابراین یک فایل به نام http.js را درون آن ایجاد می کنم.

اگر دقت کرده باشید ما درون فایل ingredients.js برای کار با درخواست های HTTP همیشه یک ساختار ثابت را داریم. اول dispatchHttp را صدا می زنیم، سپس درخواست را ارسال می کنیم (با fetch) و سپس dispatchHttp را برای دریافت response صدا می زنیم. در نهایت در صورت داشتن خطا با dispatchHttp از نوع error مسئله را حل می کنیم. بنابراین ما منطقی از کدها را داریم که می خواهیم بین کامپوننت های مختلف به اشتراک گذاشته شود اما این منطق، نوعی از منطق است که روی state برنامه تاثیر خواهد گذاشت. به همین دلیل است که نمی توانیم از یک تابع عادی استفاده کنیم. مثلا برای درخواست های fetch می توانیم یک تابع خاص تعریف کنیم که مسئول ارسال و دریافت پاسخ از سرور است اما از درون یک تابع معمولی نمی توانیم event هایی را dispatch کنیم که بعدا state را صدا زده و تابع را فراخوانی کنند. hook ها تنها توابعی هستند که می توانند این کار را انجام دهند.

اولین نکته برای تعریف کردن hook ها این است که نام hook های شما باید همیشه با useX شروع شوند به طوری که use ثابت و X نام دلخواه شما باشد. مثلا نام hook ما در این قسمت useHttp است:

const useHttp = () => {

}

export default useHttp;

این یک hook بسیار ساده است که البته فعلا کاری انجام نمی دهد. یادتان باشد که hook ها توابع عادی هستند که react با آن ها به صورت خاصی رفتار می کند به همین دلیل نامشان حتما باید به شکل useX باشد (مثلا useFetch یا useHttp و الی آخر). هر کامپوننتی که از این hook استفاده کند، طوری اجرا می شود که انگار کدهای درون hook، داخل خود کامپوننت نوشته شده اند. بنابراین اینطور نیست که یک تابع ساده باشد و کامپوننت های مختلف همان کدهای یکسان را اجرا کنند، بلکه آن کدها طوری اجرا می شوند که گویی جزئی از آن کامپوننت هستند.

در اولین قدم می خواهم reducer خودم را از فایل ingredients.js کات کرده و داخل این فایل قرار بدهم:

import { useReducer, useCallback } from 'react';

const httpReducer = (curHttpState, action) => {
  switch (action.type) {
    case 'SEND':
      return { loading: true, error: null };
    case 'RESPONSE':
      return { ...curHttpState, loading: false };
    case 'ERROR':
      return { loading: false, error: action.errorMessage };
    case 'CLEAR':
      return { ...curHttpState, error: null };
    default:
      throw new Error('Should not be reached!');
  }
};

const useHttp = () => {

}

export default useHttp;

من این reducer را خارج از hook گذاشته ام چرا که نمی خواهم با هر دوره render یک بار دیگر ساخته شود. hook شما با هر چرخه render دوباره ساخته می شود بنابراین آن را خارج از hook گذاشته ام تا آن را دوباره نسازیم. همچنین توجه داشته باشید که useReducer و useCallback را نیز import کرده ام چرا که بعدا به آن نیاز خواهیم داشت.

حالا از آنجایی که reducer را از درون ingredients.js برداشته ام دیگر کد useReducer دوم (که تابع httpReducer را صدا می زند) کار نمی کند بنابراین آن را نیز کات کرده و آن را درون hook خودم قرار می دهم:

const useHttp = () => {
    const [httpState, dispatchHttp] = useReducer(httpReducer, {
        loading: false,
        error: null
    });
}

در مرحله بعد دوباره به ingredients.js می روم و درخواست fetch درون تابع removeIngredientHandler را کات کرده و به درون hook خودم انتقال می دهم:

const useHttp = () => {
    const [httpState, dispatchHttp] = useReducer(httpReducer, {
        loading: false,
        error: null
    });

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

در حال حاضر درخواست HTTP بالا با هر بار صدا زدن hook ما ارسال می شود که قطعا مورد نظر ما نیست. برای حل این مشکل یک تابع جدید درون این hook می نویسیم تا کدهای ارسال درخواست فقط زمانی اجرا شوند که این تابع اجرا می شود:

const useHttp = () => {
  const [httpState, dispatchHttp] = useReducer(httpReducer, {
    loading: false,
    error: null,
    data: null
  });

  const sendRequest = useCallback(
    (url, method, body) => {
      dispatchHttp({type: 'SEND'});
      fetch(url, {
        method: method,
        body: body,
        headers: {
          'Content-Type': 'application/json'
        }
      })
        .then(response => {
          return response.json();
        })
        .then(responseData => {
          dispatchHttp({
            type: 'RESPONSE',
            responseData: responseData
          });
        })
        .catch(error => {
          dispatchHttp({
            type: 'ERROR',
            errorMessage: 'Something went wrong!'
          });
        });
    },
    []
  );

  return {
    isLoading: httpState.loading,
    data: httpState.data,
    error: httpState.error,
    sendRequest: sendRequest
  };
};

همانطور که می بینید من ساختار این کدها را کمی تغییر داده ام. ابتدا باید ارسال درخواست را dynamic و پویا کنیم. یعنی هر بار قصد ارسال یک درخواست خاص را نداریم بلکه ممکن است url و داده های آن برای درخواست های مختلف متفاوت باشد. مثلا شاید کسی که از hook ما استفاده می کند اصلا قصد استفاده از firebase را نداشته باشد بنابراین پارامترهای پویا را به این تابع پاس داده ام. این پارامترها شامل موارد زیر هستند:

  • url همان url ای است که درخواست به آن ارسال می شود.
  • method همان متدی است که باید به url ارسال شود (مثلا get یا Delete و ...).
  • body برای برخی از درخواست های HTTP که دارای body هستند.

بگذارید قدم به قدم این کد را برایتان توضیح بدهم. ما در ابتدا url و method و body را در درخواست fetch پیاده سازی کردیم که کار آسانی بود. قسمت body درون برخی از درخواست ها موجود است بنابراین این گزینه را در اختیار برنامه نویس قرار داده ایم. سپس به قسمت response رسیده ایم. در این قسمت باید داده های ارسال شده از سرور را به کامپوننتی بدهیم که در حال اجرای hook ما است نه اینکه خودمان درون hook آن داده را پردازش کنیم. برای این کار مقدار جدیدی به نام data را به state اولیه اضافه کرده ایم (قسمت useReducer). حالا زمانی که loading را برای حالت RESPONSE روی false می گذاریم، می توانیم این داده را نیز پاس بدهیم:

const httpReducer = (curHttpState, action) => {
    switch (action.type) {
      case 'SEND':
        return { loading: true, error: null, data: null };
      case 'RESPONSE':
        return { ...curHttpState, loading: false, data: action.responseData };
// بقیه کدها //

من در حالت SEND مقدار Data را روی null گذاشته ام چرا که در هنگام شروع درخواست هنوز data ای وجود ندارد. سپس در قسمت RESPONSE نیز آن را دریافت کرده ایم که برایتان مشخص است. به همین دلیل درون hook از response.json استفاده کرده ایم که داده های سرور را برگردانیم، سپس آن را با یک then دیگر و در قالب responseData دریافت کرده ایم و بعد از Dispatch کردن RESPONSE مقدارِ responseData را نیز برگردانده ایم.

حالا آن را در شیء ای پایانی (در قسمت آخر hook) برگردانده ایم. در واقع این مقادیر همگی درون httpState قرار داشته اند و حالا باید برگردانده شوند. چرا؟ به دلیل اینکه اگر این مقادیر را برنگردانیم، درون hook باقی می مانند و اصلا به کامپوننت مورد نظر نمی رسند. همچنین تابع sendRequest را نیز برگردانده ایم تا کامپوننت مورد نظر بتواند آن را صدا زده و درخواست خود را ارسال کند. از طرفی از آنجایی که از useCallback استفاده کرده ایم دیگر شاهد re-render های اضافی نیستیم.

حالا می توانیم به ingredients.js برویم و از این hook استفاده کنیم:

import useHttp from '../../hooks/http';

در ابتدا آن را وارد این فایل کرده ام. یادتان باشد که نام این import باید با use شروع شود. همانطور که می دانید برای صدا زدن hook نمی توانیم درون یک تابع دیگر باشیم (مثلا درون addIngredientHandler صدا زده نمی شود) بنابراین در همان ابتدای کامپوننت صدایش می زنیم:

const Ingredients = () => {
  const [userIngredients, dispatch] = useReducer(ingredientReducer, []);
  const {
    isLoading,
    error,
    data,
    sendRequest
  } = useHttp();
// بقیه کدها //

باز هم از object destructuring استفاده کرده ام تا موارد برگردانده شده از hook را دریافت کنم. حالا در قسمت JSX فایل ingredients.js که از httpState.error استفاده کرده ایم از error خالی استفاده می کنیم. چرا؟ به دلیل اینکه error را در کد بالا وارد این کامپوننت کرده ایم. بنابراین:

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

همچنین در همین قسمت به جای httpState.loading از isLoading استفاده می کنم:

  return (
    <div className="App">
      {error && <ErrorModal onClose={clearError}>{error}</ErrorModal>}

      <IngredientForm
        onAddIngredient={addIngredientHandler}
        loading={isLoading}
      />

حالا به متد removeIngredientHandler می رویم و آن را با استفاده از Hook جدیدمان می نویسیم:

  const removeIngredientHandler = useCallback(
    ingredientId => {
      sendRequest(
        `https://react-hooks-update.firebaseio.com/ingredients/${ingredientId}.json`,
        'DELETE'
      );
    },
    [sendRequest]
  );

از آنجایی که از useCallback استفاده کرده ایم من هم sendRequest را به عنوان وابستگی به آن داده ام. در حال حاضر اگر کدها را ذخیره کرده و به مرورگر بروید (البته کدهای addIngredientHandler را کامنت کنید تا به خطا برنخورید) متوجه می شوید که هنگام حذف یک آیتم از ingredient ها، آن آیتم از firebase حذف می شود اما از UI برنامه نمی رود. چرا؟ به دلیل اینکه ingredients را درون ingredient.js ویرایش و به روز رسانی نمی کنیم. در جلسه بعد این کار را انجام خواهیم داد.

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

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