ساخت یک هوک سفارشی در ری اکت

توسط: محسن درم بخت | منتشر شده در 1399/08/03 | بازدید : 6681 بار | زمان مطالعه : 15 دقیقه

این احتمال وجود دارد که در بسیاری از کامپوننت های موجود در یک اپلیکشین ری اکت (React) شما مجبور به فراخوانی API برای بازیابی داده هایی باشید که به کاربران شما نمایش داده می شوند. انجام این کار با استفاده از متد چرخه عمر componentDidMount از قبل امکان پذیر است، اما با معرفی هوک ها (Hooks)، می توانید یک هوک سفارشی بسازید که داده ها را برای شما واکِشی و کَش کند. این همان چیزی است که در این آموزش به دنبال آن خواهیم بود.

واکشی داده ها در یک کامپوننت ری اکت

پیش از معرفی هوک ها، واکشی داده های اولیه در  متد چرخه عمر componentDidMount انجام شده و بر اساس تغییرات Prop یا State در componentDidUpdate  مجددا واکشی می شدند.

نحوه کار معمولا به شکل زیر است:

import React from 'react'

class CustomFetchClassComponent extends React.Component {

    state = {
        searchQuery: null,
        data: null
    }

    fetchData = async () => {
        const url = `https://sample.com/api/endpoint?query=${this.state.searchQuery}`
        const response = await fetch(url);
        const data = await response.json();
        this.setState({ data })
    }

    async componentDidMount() {
        fetchData();
    }

    async componentDidUpdate(previousProps, previousState) {
        if (previousState.searchQuery !== this.state.searchQuery) {
            fetchData();
        }
    }

    render() {
        return this.props.children(this.state);
    }
}

به محض mount  شدن کامپوننت (یعنی وقتی که ری اکت کامپوننت را برای بار اول رندر می کند و DOM را می سازد)، متد componentDidMount فراخوانی می شود و وقتی این کار انجام شد، کاری که ما انجام دادیم این بود که درخواست جستجو را از طریق  API  ارسال کرده و براساس پاسخ دریافتی State را به روزرسانی کردیم.

از سوی دیگر ، متد چرخه ی componentDidUpdate در صورت تغییر در State  یا Prop   در کامپوننت ، فراخوانی می شود. ما برای جلوگیری از فراخوانی این متد در هر بار تغییر درState  ، واژه ی جستجو شده قبلی  را در State با درخواست فعلی مقایسه می کنیم. حال اگر از هوک به جای کامپوننت کلاسی استفاده کنیم هر دو متد چرخه عمر به روشی تمیزتر با هم ترکیب می شوند - به این معنی که برای زمان mount  شدن کامپوننت و به روزرسانی آن نیازی به دو متد چرخه عمر نخواهیم داشت.

واکشی داده ها با هوک useEffect

در تکه کد زیر به محض mount شدن کامپوننت، هوک useEffect فراخوانی می شود. اگر به اجرای مجدد این هوک بر اساس برخی تغییرات مربوط به State یا Prop نیاز داشته باشیم، باید آنها را به آرایه وابستگی منتقل کنیم (که دومین آرگومان در هوک useEffect است). مثلا آرایه ی شامل متغیر searchQuery در کد زیر در آرگومان دوم بیانگر آن است که اگر searchQuery تغییر کرد هوک را مجددا اجرا کن:

import { useState, useEffect } from 'react';

const [status, setStatus] = useState('idle');
const [searchQuery, setSearchQuery] = useState('');
const [data, setData] = useState(null);

useEffect(() => {

    if (!searchQuery) return;

    const fetchData = async () => {
        setStatus('fetching');
        const url = `https://sample.com/api/endpoint?query=${searchQuery}`
        const response = await fetch(url);
        const data = await response.json();
        setData(data);
        setStatus('fetched');
    };

    fetchData();

}, [searchQuery]);

در مثال بالا، ما searchQuery را به عنوان وابستگی به هوک useEffect منتقل کردیم. با انجام این کار ، ما به useEffect می گوییم تا تغییرات searchQuery را ردیابی کند. اگر مقدار قبلی آن با مقدار فعلی یکسان نباشد ، دوباره useEffect فراخوانی می شود.

علاوه بر آن، ما چند State دیگر را بر روی کامپوننت تنظیم می کنیم تا از طریق آن در وضعیت های مختلف قبل، حین و بعد از فراخوانی API. بتوانیم یک پیام مناسب یا آیکونی برای نشان دادن وضعیت «در حال دریافت» یا «دریافت شد» به کاربران نشان دهیم.

ساخت یک هوک سفارشی

«یک هوک سفارشی یک تابع جاوااسکریپت است که نام آن با use آغاز می شود و ممکن است هوک های دیگر درون آن فراخوانی شوند.»

واقعا هوک سفارشی چیزی جز یک تابع جاوااسکریپت نیست و به شما اجازه می دهد یک تکه کد را در بخش های مختلف اپلیکیشن خود استفاده کنید.

اجازه دهید با یک مثال ساده ی کانتر هوک را بررسی کنیم:

import { useState } from 'react'

const useCounter = (initValue = 0) => {
  // defining state
  const [count, setCount] = useState(initValue);

  // functions
  const add = () => setCount(count + 1)
  const subtract = () => setCount(count - 1)

  return { count, add, subtract };
}

export default useCounter

در اینجا ، ما یک تابع معمولی داریم که در آن یک آرگومان اختیاری وجود دارد ، مقدار آرگومان را در State مربوطه قرار می دهیم ، و همچنین متد های جمع و تفریق را که می تواند برای به روزرسانی مقدار count استفاده شود اضافه می کنیم.

در هرجای برنامه خود که به شمارنده احتیاج داشته باشیم ، می توانیم useCounter را مانند یک تابع عادی فراخوانی کرده و آرگومان مقدار اولیه را به آن پاس دهیم. وقتی حالت اولیه نداشته باشیم، عدد صفر پیش فرض ما خواهد بود.

import useCounter from './useCounter'

function App() {
  const { count, add, subtract } = useCounter(100);
  return (
    <div>
      <label>{count}</label>
      <hr />
      <button onClick={add}>افزایش</button>
      <button onClick={subtract}>کاهش</button>
    </div>
  );
}
  
export default App;

کاری که ما در اینجا انجام دادیم این بود که هوک سفارشی خود را از فایلی که در آن تعریف شده وارد (import) کرده و از آن در برنامه خود استفاده  می کنیم. ما حالت اولیه آن را روی 100 قرار می دهیم ، بنابراین هر وقت add را صدا می کنیم ، تعداد را 1 واحد افزایش می دهد و هر زمان که subtract را فراخوانی کنیم ، 1 واحد کاهش می دهد.

پیاده سازی هوک useFetch

اکنون که نحوه ایجاد یک هوک سفارشی ساده را آموختیم ، بیایید منطق خود را برای واکشی داده ها در یک هوک سفارشی پیاده سازی کنیم:

 

import { useState,useEffect } from 'react'

const useFetch = (url) => {

    const [status, setStatus] = useState('idle');
    const [data, setData] = useState(null);

    useEffect(() => {
        if (!url) return;
        
        const getData = async () => {
            setStatus('fetching');
            const response = await fetch(url);
            const data = await response.json();
            setData(data);
            setStatus('fetched');
        };
        getData();

    }, [url]);

    return { status, data };
};

تقریباً همان کاری که در بالا برای useCounter  انجام دادیم به استثنای تابعی که query را می گیرد و state و داده را برمی گرداند. و این یک هوک useFetch است که ما می توانیم در کامپوننت های مختلف در برنامه React خود استفاده کنیم.

در اینجا یکی از روش های استفاده از آن را بررسی می کنیم: 

import useFetch from './useFetch'
import { useState, useEffect } from 'react'


const [searchQuery, setSearchQuery] = useState('');

const url = searchQuery && `https://sample.com/api/endpoint?query=${searchQuery}`;

const { status, data } = useFetch(url);

در کد بالا ، اگر مقدار searchQuery موجود باشد، URL را تنظیم کرده و اگر اینگونه نباشد، مقدار undefined  را پاس می دهیم که در منطق هوک آن را کنترل می کنیم.

به خاطر سپردن (Memoizing) داده های واکشی شده

Memoization (به خاطر آوردن) تکنیکی است که ما از آن استفاده می کنیم تا در صورتی که یکبار داده ها را از یک API  واکشی کرده ایم برای استفاده مجدد همان داده ها از نسخه ی قبلی و ذخیره شده استفاده کنیم. ذخیره نتیجه برای API های  سنگین که ممکن است چندین بار در خلال برنامه یا کامپوننت به آن نیاز داشته باشیم به طرز قابل توجهی عملکرد و تجربه کاربری اپلیکیشن را ارتقا می دهد.

به کد زیر دقت کنید:

 

 

const cache = {};

const useFetch = (url) => {

    const [status, setStatus] = useState('idle');
    const [data, setData] = useState([]);

    useEffect(() => {
        if (!url) return;

        const fetchData = async () => {
            setStatus('fetching');

            if (cache[url]) {
                const data = cache[url];
                setData(data);
                setStatus('fetched');

            } else {
                const response = await fetch(url);

                // save response in cache variable;
                cache[url] = await response.json();

                setData(data);
                setStatus('fetched');
            }
        };

        fetchData();

    }, [url]);

    return { data, status };
};

در اینجا ، ما داده ها را URL های متناظر آنها نگاشت کرده ایم . بنابراین ، اگر درخواستی برای واکشی برخی از داده های موجود ارائه دهیم ، داده ها را از حافظه cache محلی خود دریافت می کنیم ، در غیر این صورت ، درخواست را به API  فرستاده و نتیجه را در حافظه cache قرار می دهیم. به این ترتیب برای دسترسی به داده هایی که به صورت محلی در دسترس ما است ، مجددا API  را فراخوانی نمی کنیم.

 تعریف متغیر مربوط به حافظه cache در اسکوپ متفاوت، با وجود اینکه ظاهرا کار می کند اما باعث می شود هوک ما مغایر اصل طراحی PureFunction باشد. در واقع خروجی تابع ما نباید تحت تاثیر عاملی به جز ورودی های تابع باشد. علاوه بر این ، ما همچنین می خواهیم مطمئن شویم كه وقتی دیگر نمی خواهیم از این کامپوننت استفاده استفاده كنیم،  متغیرها و حافظه ی مربوط به کامپوننت پاک شود.

 به کمک هوک useRef دو مشکل بالا را حل خواهیم کرد.

استفاده از useRef به عنوان روشی برای Memoization 

useRef مانند یک باکس است که می تواند یک مقدار قابل تغییر را در خصیصه ی current  خود نگه دارد.

با استفاده از useRef می توان مقادیر قابل تغییر را به راحتی ذخیره و بازیابی کرد و مقدار آن در طول چرخه حیات کامپوننت باقی خواهد ماند.

حال منطق cache  پیاده سازی شده را با useRef پیاده سازی می کنیم.

const useFetch = (url) => {

    const cache = useRef({});
    const [status, setStatus] = useState('idle');
    const [data, setData] = useState(null);

    useEffect(() => {
        if (!url) return;

        const fetchData = async () => {

            setStatus('fetching');

            if (cache.current[url]) {
                setData(cache.current[url]);

            } else {
                const response = await fetch(url);
                cache.current[url] = await response.json();
                setData(data);
            }

            setStatus('fetched');
        };

        fetchData();
    }, [url]);

    return { data, status };
};

نهایی سازی هوک

اینکه ابتدا state مربوط به داده ها تغییر کند و بعد state مربوط به fetch موجب می شود دو بار تغییرstate داشته باشیم و علاوه بر اینکه ممکن است state های ناخواسته به وجود بیاید (مثلا هنوز در وضعیت fetching باشیم ولی داده ها دریافت شده باشند)، تعداد رندرها هم افزایش می یابد. راه حل استفاده از useReducer  است. بدین ترتیب به کمک تابع dispatch تغییرات state به صورت یکجا اعمال می شود.

مساله دیگری که وجود دارد این است که در هنگامی که کامپوننت unmount می شود نباید state بروزرسانی شود در غیر این صورت با ارور زیر مواجه می شویم: "Can't perform a React state update on an unmounted component"

در روش هوک در ری اکت به جای componentWillUnmount اگر یک تابع را به عنوان خروجی useEffect برگرد انیم معادل این متد چرخه عمر عمل می کند. بنابراین عملیات پاک سازی کامپوننت یا کنسل کردن ریکوئست را میتوانی  در آن انجام دهیم.  تکه کد زیر نسخه نهایی شده ی هوک ما را نشان می دهد:

const initialState = {
    status: 'idle',
    error: null,
    data: [],
};

const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
        case 'FETCHING':
            return { ...initialState, status: 'fetching' };
        case 'FETCHED':
            return { ...initialState, status: 'fetched', data: action.payload };
        case 'FETCH_ERROR':
            return { ...initialState, status: 'error', error: action.payload };
        default:
            return state;
    }
}, initialState);

useEffect(() => {
    let cancelFetch = false;
    if (!url) return;

    const fetchData = async () => {
        dispatch({ type: 'FETCHING' });
        if (cache.current[url]) {
            dispatch({ type: 'FETCHED', payload: cache.current[url] });
        } else {
            try {
                const response = await fetch(url);
                const data = await response.json();
                cache.current[url] = data;
                if (cancelFetch) return;
                dispatch({ type: 'FETCHED', payload: data });
            } catch (error) {
                if (cancelFetch) return;
                dispatch({ type: 'FETCH_ERROR', payload: error.message });
            }
        }
    };

    fetchData();

    return function cleanup() {
        cancelFetch = true;
    };
}, [url]);

 در کد بالا متغیر cancelRequest تعیین می کند که آیا تغییرات state  انجام شود یا خیر. در صورت unmount شدن کامپوننت این مقدار false شده و در هنگام تغییرstate  با چک کردن این متغیر جلوی تغییر ناخواسته گرفته می شود.

جمع بندی

در این آموزش به صورت قدم به قدم پیاده سازی یک هوک برای دریافت داده ها از API  بررسی کردیم و یک هوک سفارشی ساختیم که مسائل  مختلفی را برای درست مدیریت کردن state ها، cache  کردن نتایج و پاک سازی کامپوننت در نظر می گیرد.

 
دوره‌های آنلاین برنامه‌نویسی لیست دوره‌ها