فصل ضمیمه: پاس دادن داده‌ها بین custom hook و کامپوننت‌ها

Appendix: Passing Data Between Custom Hooks and Components

22 بهمن 1399
فصل ضمیمه: پاس دادن داده ها بین custom hook و کامپوننت ها

در جلسه قبل توانستیم hook خودمان را بسازیم اما مشکلی وجود داشت. هنوز نمی توانیم ingredients درون کامپوننت را به روز رسانی کنیم بنابراین UI نیز پس از حذف یا اضافه کردن آیتم ها به روز رسانی نمی شد. ما می توانیم از hook ای به نام useEffect برای انجام این کار (به روز رسانی ingredients پس از حذف آیتم) استفاده کنیم. اگر یادتان باشد useEffect پس از اتمام هر چرخه اجرا می شود. در حال حاضر اگر موفق به ارسال درخواست شویم یک RESPONSE را Dispatch می کنیم که باعث می شود state تغییر کرده و کامپوننت Ingredient خودش را دوباره بسازد و re-render شکل بگیرد. در حال حاضر useEffect ما به شکل زیر است (درون فایل Ingredients.js):

  useEffect(() => {
    console.log('RENDERING INGREDIENTS', userIngredients);
  }, [userIngredients]);

اما حالا می توانیم از آن برای کاری که گفتم استفاده کنیم:

  useEffect(() => {
    dispatch({ type: 'DELETE', })
  }, [data]);

من وابستگی را data تعیین کرده ام چرا که با دریافت ثبت موفقیت آمیز یا delete مقدارش تغییر می کند. البته می بینید این کد ناقص است و هنوز id را به آن پاس نداده ایم. به نظر شما id را از کجا پیدا کنیم؟ ما می توانیم یک ویژگی به custom hook خود (فایل http.js) اضافه کنیم به نام extra که اطلاعات اضافی را داشته باشد:

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

حالا زمانی که درخواست ارسال می شود آن را در reducer قرار می دهیم:

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

// بقیه کدها //

خود درخواست send را درون متد sendRequest ارسال یا dispatch می کنیم، بنابراین در همانجا extra را از بیرون دریافت می کنم:

    const sendRequest = useCallback(
        (url, method, body, reqExtra) => {
          dispatchHttp({ type: 'SEND', extra: reqExtra });
          fetch(url, {
// بقیه کدها //

منظور من از reqExtra همان request extra است که باید به متد پاس داده شود. حالا به فایل Ingredients.js برگردید و به قسمتی بروید که از sendRequest استفاده کرده ایم. در آنجا باید reqExtra را پاس بدهیم:

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

همانطور که مشاهده می کنید ingredientId را پاس داده ایم و از آنجایی که نیازی به body نداشتیم من آن را برابر null قرار دادم. حالا به http.js برمی گردیم تا آن reqExtra را که در state ذخیره شده است، برگردانیم:

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

با این کار آن را (reqExtra) درون Ingredients.js دریافت می کنیم:

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

و در نهایت از آن در useEffect استفاده می کنیم:

  useEffect(() => {
    dispatch({ type: 'DELETE', id: reqExtra })
  }, [data, reqExtra]);

ما خودمان می دانیم که id را درون reqExtra ذخیره کرده ایم بنابراین آن را به عنوان id پاس می دهیم. بنابراین زمانی که درخواست DELETE را ارسال کنیم data تغییر می کند که باعث اجرا شدن useEffect شده و در نهایت آن هم باعث dispatch شدن درخواست DELETE شده که ingredients ما را به روز رسانی می کند. از آنجایی که تابع به reqExtra وابستگی دارد آن را هم به عنوان وابستگی وارد کرده ام. البته توجه داشته باشید که به جای انجام این کار ها می توانید از یک then ساده استفاده کنید! همانطور که می دانید ما hook خود را طوری نوشته ای که یک promise برمی گرداند بنابراین می توانستیم منطق آن را درون یک Then اجرا کنیم نه اینکه از useEffect استفاده کنیم.

قبل از تست کردن این مورد بیایید addIngredientHandler را در فایل Ingredients.js نیز کدنویسی کنیم:

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

ما در این متد نیازی به reqExtra نداریم بنابراین آن را پاس نداده ایم. باز هم مانند قبل باید به useEffect برویم تا جواب ارسال شده به آن را نیز تکمیل کنیم. در حال حاضر فقط درخواست DELETE را dispatch می کنیم که مشکل ساز است، چرا که تمام درخواست های ما DELETE نیست (مثلا همین درخواست ما، درخواست ثبت آیتم است نه حذف). برای حل این مشکل یک شرط if ساده کفایت می کند:

  useEffect(() => {
    if (reqExtra) {
      dispatch({ type: 'DELETE', id: reqExtra })
    }
  }, [data, reqExtra]);

reqExtra فقط برای درخواست های DELETE مورد استفاده قرار می گیرد بنابراین اگر وجود داشته باشد یعنی درخواست ما DELETE بوده است. همچنین برای اینکه مطمئن شویم این کد برای هر درخواست جدید کار می کند به فایل http.js می رویم و به جای اینکه reqExtra را در هنگام SEND ارسال کنیم، آن را null کرده و در هنگام دریافت RESPONSE ارسال خواهیم کرد:

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

به همین دلیل در قسمت تعریف sendRequest دیگر نیازی به ارسال کردن extra نداریم:

    const sendRequest = useCallback(
        (url, method, body, reqExtra) => {
          dispatchHttp({ type: 'SEND'});
// بقیه کدها //

بلکه هنگام دریافت RESPONSE این کار را انجام می دهیم:

// بقیه کدها //
            .then(response => {
              return response.json();
            })
            .then(responseData => {
              dispatchHttp({
                type: 'RESPONSE',
                responseData: responseData,
                extra: reqExtra
              });
            })
// بقیه کدها //

با این کار مطمئن می شویم که چک کردن reqExtra در شرط if همیشه به درستی کار خواهد کرد. حالت دیگر ارسال و ثبت آیتم بود بنابراین else را به این شرط اضافه می کنیم:

  useEffect(() => {
    if (reqExtra) {
      dispatch({ type: 'DELETE', id: reqExtra })
    } else {
      dispatch({
        type: 'ADD',
        ingredients: { id: data.name, ...ingredient }
      });
    }
  }, [data, reqExtra]);

بدین صورت می توانیم درخواست خود را ثبت کنیم. مشکل اینجاست که ما در این کد به ingredient دسترسی نداریم. ما می توانیم آن را در قالب reqExtra ارسال کنیم اما شرط if با این کار خراب خواهد شد. فعلا بیایید از همان reqExtra استفاده کنیم تا بعدا برای شرط if فکری بکنیم:

      dispatch({
        type: 'ADD',
        ingredients: { id: responseData.name, ...reqExtra }
      });

سپس ingredient را به addIngredientHandler اضافه می کنیم:

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

تا اینجا مشکلی نداریم اما شرط if کاملا بهم ریخته است. برای اینکه بین درخواست ها تفاوت قائل شویم دیگر نمی توانیم از reqExtra استفاده کنیم بنابراین باید مقدار دیگری را به hook اضافه کنیم که با آن نوع درخواست را تشخیص دهیم. به فایل http.js بروید و identifier را به آن اضافه کنید:

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

در واقع identifier قرار است «مشخص کننده» انواع درخواست ها باشد. سپس هنگام تعریف sendRequest می گوییم:

    const sendRequest = useCallback(
        (url, method, body, reqExtra, reqIdentifier) => {
          dispatchHttp({ type: 'SEND', identifier: reqIdentifier});
          fetch(url, {
// بقیه کدها //

حالا که به action پاس داده شده است باید آن را در reducer دریافت کنیم:

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

از آنجایی که identifier قرار است مشخص کننده هر درخواست باشد باید آن را به تک تک درخواست ها متصل کنیم. من از addIngredientHandler شروع می کنم:

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

از این به بعد ADD_INGREDIENTS قرار است رشته مشخص کننده نوع درخواست (identifier) باشد. برای درخواست حذف نیز درون removeIngredientHandler همین کار را می کنیم، البته identifier آن باید تفاوت داشته باشد:

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

حالا می توانیم از این identifier ها درون useEffect استفاده کنیم تا نوع درخواست ها را تشخیص دهیم. ابتدا باید آن را از useHttp دریافت کنیم. برای این کار به http.js رفته و آن را return کنید:

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

حالا این مقدار از useHttp قابل دریافت است:

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

در نهایت به useEffect می رویم تا با استفاده از identifier ها مشخص کنیم هر نوع درخواست از چه نوعی خواهد بود:

  useEffect(() => {
    if (reqIdentifier === 'REMOVE_INGREDIENT') {
      dispatch({ type: 'DELETE', id: reqExtra })
    } else if (reqIdentifier === 'ADD_INGREDIENT') {
      dispatch({
        type: 'ADD',
        ingredients: { id: responseData.name, ...reqExtra }
      });
    }
  }, [data, reqExtra, reqIdentifier]);

در حال حاضر کدهایمان مشکل دارد. useEffect پس از هر چرخه render یک بار اجرا می شود. مثلا زمانی که hook ما SEND را dispatch می کند، loading روی true می رود بنابراین تغییر می کند و این تغییر باعث render جدید در کامپوننت Ingredients می شود (به دلیل اینکه از loading درون این ingredients استفاده می کنیم). به همین دلیل باید علاوه بر identifier وضعیت loading را نیز چک کنید تا اگر در حال loading بودیم کاری نکنیم. اگر در حال loading باشیم یعنی هنوز داده ها ثبت نشده یا دریافت نشده اند. در واقع data فقط زمانی ثبت می شود که RESPONSE را دریافت کرده باشیم اما در هنگام dispatch کردن اولین action (که همان SEND است) باز هم data را داریم گرچه مقدار آن null است:

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

(کد بالا مربوط به فایل http.js است)

و مقدار null باعث خطا در برنامه ما خواهد شد. البته error نیز همین کار را می کند بنابراین باید هم identifier و هم error و هم loading را بررسی کرده و شرط if را به شکل زیر و دقیق تر بنویسیم:

  useEffect(() => {
    if (!isLoading && !error && reqIdentifer === 'REMOVE_INGREDIENT') {
      dispatch({ type: 'DELETE', id : reqExtra });
    } else if (!isLoading && !error && reqIdentifer === 'ADD_INGREDIENT') {
      dispatch({
        type: 'ADD',
        ingredient: { id: data.name, ...reqExtra }
      });
    }
  }, [data, reqExtra, reqIdentifer, isLoading, error]);

حالا کدهای ما کار می کند و می توانیم آیتم ها را اضافه یا حذف کنیم.

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

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