فصل ضمیمه: استفاده از custom hook (قسمت پایانی)

Appendix: Using Custom Hook

22 بهمن 1399
فصل ضمیمه: استفاده از custom hook (قسمت پایانی)

حالا که کار و استفاده از custom hook خود را شروع کرده ایم، باید ابتدا کمی فایل را مرتب کنیم. در حال حاضر قسمت های زیادی در کد ما وجود دارند که کامنت شده اند و ما باید آن ها را پاک کنیم. یک نمونه، کدهای درون addIngredientHandler است:

const addIngredientHandler = useCallback(ingredient => {
    sendRequest(
      'https://react-hooks-update.firebaseio.com/ingredients.json',
      'POST',
      JSON.stringify(ingredient),
      ingredient,
      'ADD_INGREDIENT'
    );
    // dispatchHttp({ type: 'SEND' });
    // fetch('https://react-hooks-update.firebaseio.com/ingredients.json', {
    //   method: 'POST',
    //   body: JSON.stringify(ingredient),
    //   headers: { 'Content-Type': 'application/json' }
    // })
    //   .then(response => {
    //     dispatchHttp({ type: 'RESPONSE' });
    //     return response.json();
    //   })
    //   .then(responseData => {
    //     // setUserIngredients(prevIngredients => [
    //     //   ...prevIngredients,
    //     //   { id: responseData.name, ...ingredient }
    //     // ]);
    //     dispatch({
    //       type: 'ADD',
    //       ingredient: { id: responseData.name, ...ingredient }
    //     });
    //   });
  }, []);

این کامنت های طولانی باعث بزرگ شدن فایل ما و همینطور شلوغ شدن کدها می شوند. زمانی که این کامنت ها را حذف کردیم (تمام کامنت ها را حذف کنید، کد بالا فقط یک مثال است) در قسمت وابستگی های (dependency) متد addIngredientHandler یک خط سبز به نشانه هشدار نمایش داده می شود. این مسئله به دلیل این است که ما یادمان رفته در جلسه قبل sendRequest را به عنوان وابستگی به آن بدهیم بنابراین این کار را نیز انجام می دهیم:

  const addIngredientHandler = useCallback(ingredient => {
    sendRequest(
      'https://react-hooks-update.firebaseio.com/ingredients.json',
      'POST',
      JSON.stringify(ingredient),
      ingredient,
      'ADD_INGREDIENT'
    );
  }, [sendRequest]);

کاری که باقی مانده است مدیریت خطاها است. در حال حاضر در فایل ingredients.js برای clearError هیچ کاری نکرده ایم:

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

بنابراین خطاها را ثبت می کنیم اما هیچ راهی برای از بین بردن آن ها نداریم. به همین دلیل یک case جدید در reducer خودمان تعریف می کنیم و نامش را clear می گذاریم. برای این کار ابتدا وارد فایل http.js شده (custom hook خودمان) و یک ثابت به نام initialState را در بالای فایل تعریف کنید:

import { useReducer, useCallback } from 'react';

const initialState = {
  loading: false,
  error: null,
  data: null,
  extra: null,
  identifier: null
}

این ثابت state اولیه ما را مشخص می کند. حالا به reducer رفته و case جدید را به شکل زیر اضافه کنید:

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

حالا از همین شیء برای صدا زدن reducer نیز استفاده می کنیم:

const useHttp = () => {
  const [httpState, dispatchHttp] = useReducer(httpReducer, initialState);
// بقیه کدها //

در مرحله بعد باید درون همین کامپوننت (useHttp) یک متد به نام clear تعریف کنید که این case جدید را برای ما Dispatch کند. در حال حاضر چنین کاری انجام نمی شود بنابراین:

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

همانطور که می بینید این متد بسیار ساده است. تنها کار آن dispatch کردن action ای به نام CLEAR است. همچنین از useCallback استفاده کرده ایم تا جلوی re-render شدن های تکراری را بگیریم. حالا در انتهای همین فایل باید clear را نیز در شیء برگردانده شده قرار دهیم:

  return {
    isLoading: httpState.loading,
    data: httpState.data,
    error: httpState.error,
    sendRequest: sendRequest,
    reqExtra: httpState.extra,
    reqIdentifer: httpState.identifier,
    clear: clear
  };

در مرحله بعد به کامپوننت Ingredients.js می رویم و clear را نیز دریافت می کنیم:

const Ingredients = () => {
  const [userIngredients, dispatch] = useReducer(ingredientReducer, []);
  const {
    isLoading,
    error,
    data,
    sendRequest,
    reqExtra,
    reqIdentifer,
    clear
  } = useHttp();

سپس دو راه دارید. یا به راحتی به متد clearError می رویم و clear را در آن صدا می زنیم:

  const clearError = useCallback(() => {
    clear();
  }, []);

و یا اینکه کل متد clearError را حذف کرده و در قسمت JSX هنگام تعریف ErrorModal به جای صدا زدن clearError، مستقیما خودِ clear را صدا می زنیم:

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

من روش دوم را انتخاب می کنم چرا که روش اول به نوعی اضافه نویسی است. برای تست کردن کدها آدرس URL برای ارسال درخواست ثبت یا حذف (addIngredientHandler یا removeIngredientHandler) را به اشتباه تغییر بدهید. مثلا:

`https://react-hooks-update.firebaseio.com/ingredients/${ingredientId}.jsxn`,

در انتهای این URL به جای json عبارت jsxn را نوشته ام که باعث بروز خطا می شود. حالا به مرورگر می رویم و یک درخواست ارسال می کنیم. پنجره خطا برای ما باز شده و همچنین اگر OK را بزنید، پنجره خطا بسته می شود بنابراین کدها به درستی کار می کنند. ما بدین شکل و به راحتی از custom hook خودمان استفاده کرده ایم!

قسمت دیگری از برنامه نیز وجود دارد که می توانیم در آن از hook خودمان استفاده کنیم: کامپوننت search. بنابراین وارد فایل search.js شده و ابتدا hook را در آن import کنید:

import Card from '../UI/Card';
import ErrorModal from '../UI/ErrorModal';
import useHttp from '../../hooks/http';
import './Search.css';

من ErrorModal را هم وارد این فایل کرده ام و بعدا برایتان توضیح می دهم چرا. سپس useHttp را صدا زده و داده های مورد نظر (مانند توابع و ...) را از آن خارج می کنیم:

const Search = React.memo(props => {
  const { onLoadIngredients } = props;
  const [enteredFilter, setEnteredFilter] = useState('');
  const inputRef = useRef();
  const { isLoading, data, error, sendRequest, clear } = useHttp();
// بقیه کدها //

اگر دقت کنید در این فایل در قسمت useEffect درخواست HTTP برای جست و جو در پایگاه داده را ارسال می کنیم. ما می توانیم تمام کدهای fetch را دور انداخته و از hook خودمان استفاده کنیم:

  useEffect(() => {
    const timer = setTimeout(() => {
      if (enteredFilter === inputRef.current.value) {
        const query =
          enteredFilter.length === 0
            ? ''
            : `?orderBy="title"&equalTo="${enteredFilter}"`;
        sendRequest(
          'https://react-hooks-update.firebaseio.com/ingredients.json' + query,
          'GET'
        );
      }
    }, 500);
    return () => {
      clearTimeout(timer);
    };
  }, [enteredFilter, inputRef, sendRequest]);

اگر به کد بالا نگاه کنید متوجه می شوید که به غیر از مشخص کردن URL و همچنین نوع درخواست (GET) هیچ کار دیگری نکرده ایم. چرا؟ به دلیل اینکه درخواست ما به body نیاز ندارد و همچنین از آنجایی که این درخواست، تنها درخواست موجود در این کامپوننت است نیازی به identifier نیز نداریم و مشخص است که درخواست برای جست و جو است. توجه داشته باشید که کدهای مربوط به onLoadIngredients و تبدیل پاسخ firebase به محتوای وب را از این قسمت کات کرده ام. می خواهم آن را در قسمت دیگری پیاده کنم. همچنین ما sendRequest را به عنوان وابستگی به این متد اضافه کرده ایم.

برای دریافت پاسخ و تبدیل آن یک useEffect دیگر می نویسیم:

  useEffect(() => {
    if (!isLoading && !error && data) {
      const loadedIngredients = [];
      for (const key in data) {
        loadedIngredients.push({
          id: key,
          title: data[key].title,
          amount: data[key].amount
        });
      }
      onLoadIngredients(loadedIngredients);
    }
  }, [data, isLoading, error, onLoadIngredients]);

ما در کد بالا با یک شرط if مشخص کرده ایم که اگر در حالت loading نبودیم و اگر خطایی نداشتیم و data (پاسخ پایگاه داده) موجود بود، این کار ها را انجام بده. چند دقیقه قبل ErroModal را نیز در این فایل import کردیم. چرا؟ به دلیل اینکه اگر درخواست با خطایی مواجه شد بتوانیم آن را به کاربر نمایش بدهیم به همین دلیل به قسمت JSX رفته و می گوییم:

  return (
    <section className="search">
      {error && <ErrorModal onClose={clear}>{error}</ErrorModal>}
      <Card>
        <div className="search-input">
// بقیه کدها //

یعنی در همان section اگر خطایی وجود داشت، ErrorModal را نمایش بده و متن آن را همان error بگذار. همچنین برای onClose باید متد clear را صدا بزنیم تا کاربر بتواند پنجره خطا را ببندد. من همچنین می خواهم تا دریافت پاسخ، یک متن ساده loading را برای کاربر نمایش بدهم بنابراین آن را هم به شکل زیر به قسمت JSX اضافه می کنم:

  return (
    <section className="search">
      {error && <ErrorModal onClose={clear}>{error}</ErrorModal>}
      <Card>
        <div className="search-input">
          <label>Filter by Title</label>
          {isLoading && <span>Loading...</span>}
          <input
// بقیه کدها //

همانطور که مشاهده می شود فقط یک متن ساده loading… را نمایش داده ایم. خود spinner را اضافه نکرده ام چرا که صفحه را به هم می ریزد (جا نمی شود) اما اگر دوست داشتید می توانید خودتان با تغییر کدهای CSS آن را اضافه کنید. این قسمت، آخرین قسمت از دوره آموزشی react بود. امیدوارم برایتان مفید بوده باشد.

دانلود سورس کد این فصل

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

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