فصل ضمیمه: آشنایی با useReducer

Appendix: useReducer

22 بهمن 1399
فصل ضمیمه: آشنایی با useReducer

اگر جلسات را به طور منظم دنبال کرده باشید می دانید که در حال حاضر سه state مختلف داریم که همگی تا حدی به هم مربوط هستند، تمام آن ها در مورد درخواست های HTTP ما هستند:

  const [userIngredients, setUserIngredients] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState();

هر زمان که درخواستی را ارسال کنیم (اضافه کردن به پایگاه داده یا حذف آن یا خواندن از آن) userIngredients تغییر می کند. isLoading برای صبر کردن تا دریافت پاسخ ایجاد شده و اگر پاسخ ما با خطا روبرو شود error وارد صحنه می شود. بدین ترتیب می فهمیم که state ما به درخواست های HTTP اختصاص داده شده است و با اینکه از هم مستقل هستند اما در بسیاری از اوقات در یک تابع هر دو را با هم تغییر می دهیم. مانند قسمت catch در removeIngredientHandler:

catch(error => {
      setError('Something went wrong!');
      setIsLoading(false);
    });

سوال: ما در کد بالا دو state را پشت سر هم تغییر می دهیم. آیا این مسئله به معنای دو بار render شدن کامپوننت ما است؟

پاسخ: خیر، در react چیزی به نام state batching وجود دارد که یعنی به روز رسانی های state جمع شده و همگی یکباره اجرا می شوند تا کامپوننت های ما فقط یک بار render شوند.

با این همه مدیریت این State ها به صورت جداگانه آزار دهنده است و اگر برنامه خود را پیشرفته تر کنیم (مثلا یکی از State ها وابسته به state دیگری باشد) کار ما خیلی سخت تر خواهد شد. به همین دلیل بهتر است به جای useState از useReducer استفاده کنیم! من دستور import درون فایل Ingredients.js را به شکل زیر ویرایش می کنم:

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

همانطور که در فصل های گذشته صحبت کردیم، reducer ها توابعی هستند که ورودی خاصی را دریافت کرده و output خاصی را نیز برمی گردانند. البته توجه داشته باشید با این که useReducer نام مشابهی با اجزا کتابخانه redux دارد اما در اصل هیچ ارتباطی با redux ندارد.

اولین مرحله از کار ما تعریف یک reducer است. معمولا reducer ها را خارج از کامپوننت تعریف می کنند (مثلا اینجا خارج از Ingredients) تا با هر بار render شدن کامپوننت، Reducer ما نیز دوباره ساخته نشود:

const ingredientReducer = (currentIngredients, action) => {

};

const Ingredients = () => { // کامپوننت ما از این قسمت شروع می شود // 
// بقیه کدها //

همانطور که می بینید این reducer یک تابع است که پارامترهایش به صورت خودکار و توسط react پاس داده می شوند. پارامتر اول همان state شما است که من نام currentIngredients (محتویات فعلی) را برایش انتخاب کرده ام و پارامتر دوم نیز action ما است که یک شیء است و مخصوص به روز رسانی state می باشد. با این مفاهیم در فصل های گذشته و هنگام کار با کتابخانه redux آشنا شدیم اما توجه داشته باشید که این hook هیچ ارتباطی با کتابخانه redux ندارد.

Action ها معمولا یک خصوصیت به نام type دارند و حالت های مختلف درخواست را ارائه می کنند تا بر اساس آن state را به روز رسانی کنیم. به طور مثال:

const ingredientReducer = (currentIngredients, action) => {
  switch (action.type) {
    case 'SET':
    case 'ADD':
    case 'DELETE':
    default:
      throw new Error('Should not get there!');
  }
};

من سه case برای ثبت (SET) و اضافه کردن (ADD) و حذف کردن (DELETE) دارم و در نهایت default را برای موارد دیگر تعیین کرده ام. حتما می دانید که نام این case ها بستگی به شما دارد و مثلا می توانید به جای SET هر نام دیگری را انتخاب کنید که در action.type نوشته باشید. همانطور که قبلا گفتم reducer ها باید مقداری را return کنند، بنابراین من نیز همین کار را انجام می دهم:

  switch (action.type) {
    case 'SET':
      return action.ingredients;

اگر قرار باشد SET را صدا بزنیم یعنی می خواهیم محتویات را در state خود تغییر داده و محتویات جدیدی را ثبت کنیم. به همین دلیل من محتویات جدید را از action گرفته و return می کنم (باید هنگام تعریف action این محتویات جدید را در آن قرار بدهیم، بعدا این کار را خواهیم کرد). مورد بعدی ADD است که در آن می خواهیم چیزی را به State قبلی اضافه کنیم نه اینکه آن را حذف کرده و یک state جدید به جایش بگذاریم، بنابراین باید حتما ابتدا از آن یک کپی بگیریم تا بتوانیم state قدیمی را با State جدید جمع بزنیم:

  switch (action.type) {
    case 'SET':
      return action.ingredients;
    case 'ADD':
      return [...currentIngredients, action.ingredient];

در نهایت در مورد DELETE باید موارد مورد نظر را حذف کنیم، بنابراین:

const ingredientReducer = (currentIngredients, action) => {
  switch (action.type) {
    case 'SET':
      return action.ingredients;
    case 'ADD':
      return [...currentIngredients, action.ingredient];
    case 'DELETE':
      return currentIngredients.filter(ing => ing.id !== action.id);
    default:
      throw new Error('Should not get there!');
  }
};

در این حالت id محتویات State را با id درون action (همان id محتوایی که باید حذف شود) مقایسه می کنیم و اگر id محتوای state با id درون action یکی نبود، باقی خواهد ماند و در غیر این صورت حذف خواهند شد.

این reducer به تنهایی کاری انجام نمی دهد چرا که هنوز عملیاتی نشده است. برای اجرای آن باید به درون کامپوننت خود رفته و با useReducer آن را صدا بزنیم. در واقع useReducer یک آرگومان اجباری می گیرد که همان reducer تعریف شده توسط ما است و یک آرگومان اختیاری هم دارد که State اولیه خواهد بود. همچنین در پاسخ به شما یک آرایه برمی گرداند که اولین عضو آن state ما (همان userIngredients) و دومین عضو نیز یک تابع dispatch است:

const Ingredients = () => {
  const [userIngredients, dispatch] = useReducer(ingredientReducer, []);
  // const [userIngredients, setUserIngredients] = useState([]);

از آنجایی که state اولیه را با useReducer تعیین کرده ایم دیگر نیازی به استفاده از useState نیست بنابراین آن را کامنت کرده ام. حالا دیگر نمی توانیم از setUserIngredients استفاده نماییم و باید به جای آن از dispatch استفاده نماییم:

  const filteredIngredientsHandler = useCallback(filteredIngredients => {
    // setUserIngredients(filteredIngredients);
    dispatch({ type: 'SET', ingredients: filteredIngredients });
  }, []);

ابتدا setUserIngredients را کامنت کرده ام و سپس شیء action را dispatch کرده ام. حالا بر اساس کدهایی که در reducer نوشته ایم باید برای SET اطلاعاتی ارسال کنیم (ingredients جدید). توجه داشته باشید که با برگرداندن state جدید کامپوننت ما re-render (دوباره render) می شود.

این کار را در تمام قسمت های دیگر برنامه نیز انجام می دهیم:

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

و همچنین در قسمت زیر:

  const removeIngredientHandler = ingredientId => {
    setIsLoading(true);
    fetch(
      `https://react-hooks-update.firebaseio.com/ingredients/${ingredientId}.json`,
      {
        method: 'DELETE'
      }
    )
      .then(response => {
        setIsLoading(false);
        // setUserIngredients(prevIngredients =>
        //   prevIngredients.filter(ingredient => ingredient.id !== ingredientId)
        // );
        dispatch({ type: 'DELETE', id: ingredientId });
      })
      .catch(error => {
        setError('Something went wrong!');
        setIsLoading(false);
      });
  };

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

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

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