فصل ضمیمه: کار با useReducer برای stateهای HTTP

Appendix: Work with useReducer for HTTP States

22 بهمن 1399
فصل ضمیمه: کار با useReducer برای state های HTTP

در قسمت قبل متوجه شدیم که useReducer باعث خواناتر شدن و زیباتر شدن کدهای ما می شود و همچنین کار ما را به عنوان یک توسعه دهنده ساده تر می کند. در چنین حالتی جریان کاری داده های ما به سادگی قابل تشخیص است و دیگر دچار سردرگمی نمی شویم. بنابراین زمانی که state شما پیچیده است و یا اینکه state هایتان به هم وابسته هستند (یا حتی به نسخه قبلی state وابسته هستند) بهترین حالت استفاده از reducer ها است.

همانطور که می دانید دو state دیگر در برنامه ما باقی مانده است که از reducer ها استفاده نمی کنند: Error و isLoading که هر دو مرتبط با یکدیگر هستند (مخصوص ارسال درخواست های HTTP اند). به همین دلیل من یک reducer دیگر در همان فایل Ingredients.js ایجاد می کنم تا مسئول مدیریت این دو state باشد:

const httpReducer = (curHttpState, action) => {

};

همچنین cur مخفف current (به معنای «فعلی») است، گرچه که تمامی این نام ها به سلیقه شما بستگی دارد و می توانید آن را تغییر بدهید. در مرحله بعد باید فکر کنیم در هنگام کار با درخواست های HTTP چه action هایی مورد نیاز ما خواهد بود؟ اولین مرحله همیشه ارسال درخواست است بنابراین «ارسال درخواست» خودش می شود یک action جداگانه. Action بعدی ما زمانی است که پاسخ برای ما ارسال می شود یا اینکه درخواست ما به خطا برمی خورد. بنابراین در این حالت سه case مختلف داریم و باید آن ها را در reducer بالا پیاده سازی کنیم:

const httpReducer = (curHttpState, action) => {
  switch (action.type) {
    case 'SEND':
    case 'RESPONSE':
    case 'ERROR':
    default:
  }
};

بر اساس چیزی که گفتم سه حالت ارسال (send) و دریافت پاسخ (response) و بروز خطا (error) را داریم و case آخر نیز Default است که هیچ وقت نباید اجرا شود چرا که خود ما این action ها را dispatch می کنیم و نباید به جز case های تعریف شده چیزی را ارسال کنیم.

قبل از اینکه کدهای این قسمت را تکمیل کنم، باید useReducer را از درون کامپوننت صدا بزنیم تا موارد لازم برای کار را داشته باشیم:

const Ingredients = () => {
  const [userIngredients, dispatch] = useReducer(ingredientReducer, []);
  const [httpState, dispatchHttp] = useReducer(httpReducer, {
    loading: false,
    error: null
  });
  // const [userIngredients, setUserIngredients] = useState([]);
  // const [isLoading, setIsLoading] = useState(false);
  // const [error, setError] = useState();

اولین دستور useReducer را که در جلسه قبل نوشته بودیم. دومین دستور useReducer را الان اضافه کرده ام. همانطور که گفتم پارامتر دوم useReducer وضعیت اولیه state برنامه را در شروع کار مشخص می کند که من آن را برابر False برای loading و null برای error قرار داده ام. با این کار دیگر نیازی به دستورات useState نیست و همانطور که در کد بالا مشاهده می کنید این قسمت ها را کامنت کرده ام. همچنین از آنجایی که قبلا برای useReducer قبلی از متغیری به نام dispatch استفاده کردیم دیگر نمی توانیم دوباره از آن استفاده کنیم و من نام Dispatch را برای useReducer دوم برابر با dispatchHttp قرار داده ام تا تداخل نام به وجود نیاید. از آنجایی که دیگر از useState استفاده نمی کنیم، نیازی به import کردن آن نیست و باید آن را از دستور import حذف کرد:

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

حالا به reducer برمی گردیم. زمانی که درخواستی را ارسال می کنیم باید loading را روی true گذاشته تا علامت Spinner نمایش داده شود (کاربر باید منتظر پاسخ بماند) و error را روی null بگذاریم چرا که هنوز پاسخی دریافت نشده که بخواهد خطا باشد. بنابراین:

const httpReducer = (curHttpState, action) => {
  switch (action.type) {
    case 'SEND':
      return { loading: true, error: null };

در مرحله بعد اگر پاسخی دریافت کنیم باید loading را غیر فعال کنیم و خطا را نیز روی null می گذارم:

const httpReducer = (curHttpState, action) => {
  switch (action.type) {
    case 'SEND':
      return { loading: true, error: null };
    case 'RESPONSE':
      return { ...curHttpState, loading: false };

من curHttpState را کپی کرده ام. چرا؟ به دلیل اینکه error از اول هم null بوده است بنابراین نیازی نیست دوباره آن را روی همان مقدار قبلی تنظیم کنیم. زمانی که state قبلی ما (curHttpState) کپی می شود خودش دارای loading است اما از آنجایی که ما نیز loading را مجددا تنظیم کرده ایم، مقدار قبلی را overwrite می کند.

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

const httpReducer = (curHttpState, action) => {
  switch (action.type) {
    case 'SEND':
      return { loading: true, error: null };
    case 'RESPONSE':
      return { loading: false, error: null };

هر دو کد دقیقا معادل یکدیگر هستند. برای خطا یا Error نیز می توان گفت:

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 };

errorMessage قرار است یکی از خصوصیات action ارسالی با تایپ ERROR باشد که بعدا آن را می نویسیم. این errorMessage مسئول نگهداری پیام خطا خواهد بود. در نهایت یک case را نیز برای ClearError تعریف می کنیم:

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

تنها کار clearError نیز حذف خطا می باشد بنابراین error را روی null می گذاریم. حالا باید هر جایی از setIsLoading و setError استفاده کرده ایم، کدها را حذف کرده و از روش جدید reducer استفاده کنیم. اولین قسمت addIngredientHandler است:

  const addIngredientHandler = 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 }
        });
      });
  };

قسمت بعدی نیز removeIngredientHandler می باشد:

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

در نهایت در متد clearError نیز همین کار را می کنیم:

  const clearError = () => {
    dispatchHttp({ type: 'CLEAR' });
  };

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

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

      <IngredientForm
        onAddIngredient={addIngredientHandler}
        loading={httpState.loading}
      />
// بقیه کدها //

حالا می توانید با خیال راحت کدهایتان را تست کنید. نباید هیچ نقصی در کدهایتان وجود داشته باشد.

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

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