راهنمای کامل React Hooks

React Hooks

30 آبان 1400
راهنمای کامل React Hooks

هوک های react در نسخه 16.8 معرفی شدند و قابلیت هایی را به ما دادند که قبل از آن فقط در کامپوننت های کلاس محور در دسترس بودند. هوک ها به شما اجازه می دهند کد های کمتری بنویسید که به نوع خود باعث می شود حجم نهایی کد ها کمتر شده و طبیعتا سرعت برنامه شما افزایش پیدا کند. هر هوک یک تابع ساده است که با کلمه use شروع می شود و یک یا چند داده یا متد را به شما می دهد.

سطح مقاله: این مقاله برای افرادی طراحی شده است که تا حدی با react آشنا هستند و حالا می خواهند به طور خاص روی هوک های آن کار کنند.

ما در این مقاله آموزشی می خواهیم شما را با انواع این هوک ها آشنا کنیم:

  • useRef
  • useState
  • useReducer
  • useEffect
  • useCallback
  • useMemo
  • useContext

پس از اینکه با این هوک ها آشنا شدیم نگاهی به نحوه نوشتن هوک های شخصی نیز می اندازیم.

۱. قانون کلی هوک ها

زمانی که صحبت از استفاده از هوک ها می شود باید به دو مسئله توجه ویژه داشته باشید. اولین نکته، این است که هوک ها فقط در کامپوننت های تابع محور قابل استفاده هستند:

// ✅ استفاده صحیح از هوک

const hook = () => {

    const [state, setState] = useState({});

};




// ✅ استفاده صحیح از هوک

function hook() {

    const [state, setState] = useState({});

}




// 🔴 استفاده غیر صحیح از هوک

class Hook extends React.Component {

    const [state, setState] = useState({});

}

مسئله دوم این است که هوک ها باید در بالاترین سطح کامپوننت شما صدا زده شوند، یعنی نمی توانید آن ها را در یک شرط صدا بزنید یا از آن ها در حلقه ها یا توابع تو در تو استفاده کنید. مثال:

// ✅ استفاده صحیح از هوک

const hook = () => {

    const [state, setState] = useState({});

};




// 🔴 استفاده غیر صحیح از هوک

const hook = () => {

    if (user.loggedIn) {

        const [state, setState] = useState({});

    }

};




// 🔴 استفاده غیر صحیح از هوک

const hook = () => {

    searchResults.map(result => {

        const [state, setState] = useState(result);

    });

};

در صورتی که یک برنامه نویس تازه کار هستید پیشنهاد می کنم از یک linter مانند eslint-plugin-react-hooks استفاده کنید تا این موارد را به شما تذکر بدهد.

۲. هوک useRef

این هوک یکی از ساده ترین هوک های موجود در کل react است. معمولا از این هوک برای ذخیره ارجاعات به عناصر DOM استفاده می شود. احتمالا بعضی از شما با React.createRef در کامپوننت های کلاس محور آشنا باشید که همین کار را می کند. در ابتدا باید بدانید که ref ها در react چه هستند. یک ref یک ارجاع ساده به یک عنصر HTML در DOM است. اگر ما خصوصیت ref را به یک عنصر بدهیم، می توانیم بعدا آن را تحت نظر گرفته و به اصطلاح یک ارجاع به آن داشته باشیم. چطور ممکن است به چنین چیزی نیاز داشته باشیم؟ مثلا برای فوکس کردن یک متن یا انتخاب بخشی از متن یا اجرای یک انیمیشن و الی آخر.

زمانی که از ref را روی یک عنصر خاص مشخص می کنید، سریعا خصوصیتی به نام current روی آن تعریف می شود که همان node مورد نظر در HTML است. مشکل اینجاست که ما نمی توانیم از ref ها در کلاس های تابع محور استفاده کنیم بنابراین هوک useRef ایجاد شد. هر چیزی را که به این هوک پاس بدهید به عنوان مقدار اولیه برای آن node در نظر گرفته می شود. به مثال زیر توجه کنید:

import React, { useRef } from 'react';




const FocusInput = () => {

    const inputElement = useRef(null);




    const onClick = () => {

                  // در اینجا به فیلد اینپوت متنی ما اشاره می کند current

        inputElement.current.focus();

    };




    return (

        <React.Fragment>

            <input ref={inputElement} type="text" />

            <button onClick={onClick}>Focus input</button>

        </React.Fragment>

    );

};

من در کد بالا مقدار اولیه useRef را null داده ام چرا که نمی خواهم input ما هیچ مقدار اولیه ای داشته باشد. اگر به قسمت render نگاه کنید متوجه می شوید که یک input و یک button داریم. با کلیک روی button متد onClick اجرا می شود. این متد current را گرفته (که در این مثال همان input می شود) و متد focus را روی آن اجرا می کند.

۳. هوک useState

useState یکی از رایج ترین هوک های حال حاضر در react بوده و معادل setState در کامپوننت های کلاس محور می باشد. شما هر زمان که بخواهید به کامپوننت خود state بدهید از این هوک استفاده می کنید. مقدار پاس داده شده به آن نیز همان مقدار اولیه state خواهد بود.

این هوک همیشه یک آرایه با دو عنصر را برمی گرداند:

  • عنصر اول همان state شما است.
  • عنصر دوم تابعی است که با استفاده از آن می توانید state را تغییر بدهید. این تابع معمولا با نام set شروع شده و سپس نام state خود را می آورید.

شاید بپرسید آیا نمی توانیم شیء state را مستقیما ویرایش کنیم؟ چرا نیاز به یک تابع جداگانه برای ویرایش آن داریم؟ باید توجه کنید که ویرایش مستقیم state باعث بروز ده ها مشکل در برنامه شما می شود بنابراین به هیچ عنوان اجازه ویرایش مستقیم آن را ندارید.

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

import React, { useState } from 'react';




const Input = () => {

    const [input, setInput] = useState('');

    const changeInput = e => setInput(e.target.value);




    return <input type="text" value={input} onChange={changeInput} />;

};

من در این مثال مقدار اولیه input را خالی گذاشته ام و سپس value آن را روی state خودمان (متغیر input) قرار داده ام. حالا با هر بار تایپ در این input مقدار آن را با جاوا اسکریپت به روز رسانی می کنیم.

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

const Input = () => {

    const [input, setInput] = useState(() => getComputationHeavyState());

    const changeInput = e => setInput(e.target.value);




    return <input type="text" value={input} onChange={changeInput} />;

};

معمولا از این روش زمانی استفاده می شود که محاسبه state خاص شما بسیار سنگین باشد و بخواهید فقط یک بار آن را اجرا کنید.

همچنین اگر فرآیند به روز رسانی state شما پیچیده است، می توانید یک تابع را به setInput پاس بدهید:

const Input = () => {

    const [input, setInput] = useState('');

    const changeInput = e => setInput(e => e.target.value);




    return <input type="text" value={input} onChange={changeInput} />;

};

در این حالت حتما باید state جدید و ویرایش شده را return کنید.

نکته بسیار مهم دیگری نیز وجود دارد. برخلاف setState در کامپوننت های کلاس محور، useState اشیاء را به صورت خودکار ادغام نمی کند. یعنی اگر state شما یک شیء باشد و بخواهید فقط یک فیلد خاص از آن را ویرایش کنید باید خودتان به صورت دستی فیلد های دیگر را حفظ کرده و نهایتا در state جدید ادغام کنید. به مثال زیر توجه کنید:

// ✅ اولیه و استفاده صحیح از آنstate تعیین

const [state, setState] = useState({

    name: 'John Doe',

    email: 'john@doe.com'

});




setState({

    ...state,

    name: 'Jane Doe'

});




// 🔴 state روش غلط به روز رسانی

const [state, setState] = useState({

    name: 'John Doe',

    email: 'john@doe.com'

});




setState({ name: 'Jane Doe' });

مشکل روش دوم این است که فقط setState را صدا زده است. با این کار setState به صورت کامل شیء state را پاک می کند و فقط  خصوصیت name باقی می ماند (خصوصیت email حذف می شود).

در ضمن هیچ محدودیتی در تعداد دفعات استفاده از useState در یک کامپوننت وجود ندارد:

const [name, setName]   = useState('John Doe');

const [email, setEmail] = useState('john@doe.com');




setName('Jane Doe');

setEmail('jane@doe.com');

۴. هوک useEffect

همانطور که می دانید کامپوننت های react دارای چرخه زندگی (lifecycle) هستند. در کامپوننت های کلاس محور هر بخش به صورت یک متد componentDidMount یا componentWillUnmount یا غیره در دسترس ما بود اما در کامپوننت های تابع محور چطور؟

انجام mutation یا subscription یا استفاده از تایمر ها یا log کردن داده ها و به طور کل عوارض جانبی (side effect) در بدنه اصلی کامپوننت های تابعی مجاز نیستند. چرا؟ به دلیل اینکه باعث ایجاد مشکلات و باگ های زیادی می شوند. در چنین حالتی هوک useEffect زمانی اجرا می شود زمانی اجرا می شود که render صفحه تمام شده باشد.

بیایید یک مثال ساده را بررسی کنیم:

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




export default function App() {

    const [count, setCount] = useState(0);




    useEffect(() => {

        console.log('rendered');

    });




    return (

        <div className="App">

            <div>{Array(count).fill('🥑').join(',')}</div>

            <button onClick={() => setCount(count + 1)}>Add avocado</button>

        </div>

    );

}

این کامپوننت لیستی از آووکادو ها را نشان می هد. از طرفی با هر بار کلیک روی button متد setCount اجرا شده و یک واحد به تعداد آووکادو ها اضافه می کند بنابراین طبق قوانین react این کامپوننت re-render می شود و useEffect دوباره اجرا می شود. چرا؟ این فرآیند re-render شدن و قوانین مخصوص آن است که تحت کنترل ما نیست و توسط react انجام می شود. به همین دلیل است که گفته می شود side-effect ها یا عارضه های جانبی (درخواست های HTTP و تعریف تایمر ها و غیره) نباید در بدنه اصلی کامپوننت های تابعی باشند چرا که باعث render شدن چندباره آن ها خواهند شد.

با این حساب چطور می توانیم عوارض جانبی را از بخش rendering در react جدا کنیم؟ اینجاست که هوک useEffect به کمک ما می آید. من در کد بالا فقط یک log ساده را در useEffect انجام داده ام. از طرفی ما می دانیم که useEffect در کد بالا اصلا از state استفاده نمی کند بنابراین تغییر کردن state نباید باعث اجرای دوباره آن (log شدن دوباره) شود. زمانی که useEffect فقط آرگومان اول خود که یک تابع است را می گیرد، با هر بار re-render شدن کامپوننت useEffect نیز اجرا می شود.

اگر بخواهیم این اتفاق نیفتد چطور؟ در این حالت باید به عنوان آرگومان دوم آرایه ای از وابستگی های useEffect را مشخص کنیم:

useEffect(() => {

    console.log('component mounted');

}, []);

از آنجایی که useEffect در کد ما هیچ وابستگی ندارد (یک log ساده است) یک آرایه خالی را پاس داده ایم بنابراین useEffect فقط یک بار اجرا شده و در re-render های بعدی اجرا نمی شود. اگر می خواستیم useEffect را با تغییر count دوباره اجرا کنیم باید آن را به آرایه بالا پاس می دادیم:

useEffect(() => {

    console.log('rendered');

}, [count]);

در نظر داشته باشید که من برای سادگی درک مطلب از یک console.log ساده استفاده کرده ام اما در اصل useEffect برای انجام عملیات هایی است که باعث ایجاد عارضه می شوند. مشهور ترین این عملیات ها ارسال درخواست های HTTP هستند.

همچنین زمانی که کارتان با یک عارضه تمام می شود بهتر است درون useEffect یک تابع را نیز return کنید تا مموری اشغال شده توسط آن آزاد شود. مثلا:

useEffect(() => {

    console.log('component mounted');




    return () => {

        console.log('component unmounted');

    };

}, []);

این مثال نیز یک log ساده است و مموری را آزاد نکرده ایم اما قالب اصلی استفاده از این تابع به شکل بالا است. یک مثال واقعی تر unsubscribe کردن از یک رویداد (event) است:

useEffect(() => {

  const subscription = props.source.subscribe();

  return () => {

    // subscription تمیز کردن و حذف

    subscription.unsubscribe();

  };

});

این تابع برگردانده شده قبل از حذف کامپوننت اجرا می شود تا مموری را آزاد کند و مشکلاتی مانند نشت حافظه رخ ندهد. همچنین اگر یک کامپوننت چندین بار render شود، این تابع باعث تمیز شدن مموری قبل از render بعدی می شود.

۵. هوک useCallback

در علوم کامپیوتر memoization یا «به خاطر سپاری» به فرآیندی گفته می شود که طی آن نتیجه اجرای توابع سنگین ذخیره می شود تا اگر بعدا همان ورودی ها را دریافت کردیم عملیات سنگین را دوباره انجام ندهیم بلکه پاسخ کش شده را مستقیما برگردانیم. هوک useCallback یک callback را برمی گرداند که memoized شده است! این هوک در واقع مانند useEffect است چرا که یک تابع را به عنوان آرگومان اول خود می گیرد و آرایه ای از وابستگی هایش را نیز به عنوان آرگومان دوم دریافت می کند.

معمولا زمانی از این هوک استفاده می شود که بخواهیم کامپوننت های فرزند را بهینه سازی کنیم تا تعداد re-render های اتفاق افتاده به دلیل callback ها را کاهش بدهیم. به مثال زیر توجه کنید:

import React, { useState } from 'react';




const Avocado = () => {

    const [count, setCount] = useState(0);




    const addAvocado = () => {

        setCount(count + 1);

    };




    return (

        <React.Fragment>

            <Addvocado add={addAvocado} />

            <div>{Array(count).fill('🥑').join(',')}</div>

        </React.Fragment>

    );

};




const Addvocado = ({ add }) => {

    console.log('component re-rendered');




    return <button onClick={add}>Add avocado</button>;

};

در این مثال دو کامپوننت را داریم:

  • کامپوننت اصلی (پدر) که Avocado نام دارد.
  • کامپوننت فرزند که Addvocado نام دارد.

کامپوننت فرزند فقط یک button را برای ما نمایش می دهد که متن Add avocado را دارد و متد addAvocado را اجرا می کند. این متد نیز خودش یک واحد به تعداد آووکادو ها اضافه می کند بنابراین با هر بار کلیک کردن روی دکمه Add avocado یک عدد به تعداد آووکادو ها اضافه می شود بنابراین state تغییر کرده است. مشکل اینجاست که با تغییر state کامپوننت ما و همچنین کامپوننت فرزند آن re-render می شوند. چرا؟ این رفتار پیش فرض react است که اگر کامپوننت پدر re-render شود کامپوننت های فرزند نیز re-render می شوند. برای اثبات این موضوع یک دستور log ساده با متن component re-rendered را درون کامپوننت فرزند گذاشته ام. اگر کد بالا را اجرا کرده و روی دکمه کلیک کنید، هر بار متن component re-rendered دوباره چاپ می شود.

این در حالی است که اصلا نیازی به re-render شدن کامپوننت فرزند نیست. چرا؟ به دلیل اینکه کامپوننت فرزند اصلا از state استفاده نمی کند و هیچ state ای در آن تغییر نکرده است. کامپوننت فرزند فقط یک button ساده است.

برای حل این مشکل می توانیم از useCallback استفاده کرده و متد addAvocado را به شکل زیر بازنویسی کنیم:

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




// کد های قبلی




const memoizedAddvocado = useCallback(() => {

    setCount((c) => c + 1);

}, [setCount]);

با انجام این کار فقط زمانی تابع تغییر می کند که setCount تغییر کرده باشد. البته از آنجایی که این تابع را memoized کرده ایم دیگر نمی توانیم به شکل عادی از setCount استفاده کنیم و حتما باید یک تابع را به آن پاس بدهیم که state جدید را return می کند (دقیقا کاری که در کد بالا کرده ایم).

این کار به تنهایی مشکل را حل نمی کند، بلکه فقط قدم اول است. قدم بعدی ما این است که کامپوننت فرزند را در React.memo قرار بدهیم تا react متوجه بشود که در حال استفاده از تابعی memoized شده هستیم. با این حساب باید کامپوننت Addvocado را بدین شکل ویرایش کنیم:

const Addvocado = React.memo(({ add }) => {

    console.log('component re-rendered');




    return <button onClick={add}>Add avocado</button>;

});

از این به بعد کامپوننت فرزند دیگر re-render نخواهد شد.

۶. هوک useMemo

این هوک یکی دیگر از هوک هایی است که برای بهینه سازی استفاده می شود. useCallback یک callback را از شما می گرفت و آن را memoize می کرد اما useMemo یک مقدار را از شما گرفته و آن را memoize می کند. به مثال زیر توجه کنید:

import { useMemo } from 'react';




const memoizedValue = useMemo(() => getFibonacciArray(length), [length]);

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

نکته: به عنوان یک قانون کلی گفته می شود که عملیات های بهینه سازی مانند useMemo یا useCallback را فقط هنگامی انجام بدهید که با مشکل سرعت در برنامه خود مواجه هستید. انجام بهینه سازی در حالی که به آن نیاز ندارید ممکن است باعث بروز مشکلاتی در برنامه هایتان بشود.

۷. هوک useReducer

هوک useReducer یک هوک پیشرفته است که می تواند جایگزین useState باشد و معمولا زمانی از آن استفاده می شود که state برنامه شما پیچیده باشد. این هوک شباهت زیادی به پکیج Redux دارد و دو آرگومان قبول می کند: یک تابع reducer و یک مقدار اولیه برای state. خود تابع reducer دو آرگومان می گیرد: state و action و سپس state جدید را برمی گرداند. ساختار کلی استفاده از این هوک معمولا به شکل زیر است:

import { useReducer } from 'react';




const reducer = (state, action) => {




};




const App = () => {

    const [state, dispatch] = useReducer(reducer, initialState);




    return (...);

};

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

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

const reducer = (state, action) => {

    switch (action.type) {

        case 'add':

            return state + 1;

        case 'remove':

            return state - 1;

        default:

            return state;

    }

};




const App = () => {

    const [state, dispatch] = useReducer(reducer, 0);




    return (

        <React.Fragment>

            <button onClick={() => dispatch({ type: 'add' })}>Addvocado</button>

            <button onClick={() => dispatch({ type: 'remove' })}>Removocado</button>

            <div>{Array(state).fill('🥑').join(',')}</div>

        </React.Fragment>

    );

};

شما می توانید هر منطقی را درون تابع reducer خود بنویسید. من در اینجا از یک دستور switch استفاده کرده ام که دو حالت اضافه کردن (add) و کم کردن (remove) آووکادو ها را دارد. توجه کنید که من در همینجا مشخص کرده ام که action باید یک شیء باشد چرا که به دنبال خصوصیت type در آن هستم. در مرحله بعدی وارد کامپوننت شده و از useReducer استفاده می کنیم. همانطور که گفته بودم هر چیزی که به dispatch پاس بدهید همان action شما خواهد بود بنابراین من type را پاس داده ام.

در نظر داشته باشید که هیچ الزامی به شیء بودن action نیست بلکه من این کار را برای نشان دادن حالتی پیچیده تر انجام دادم. شما می توانید از مقادیر primitive مانند رشته ها و اعداد نیز استفاده کنید. مثال:

const reducer = (state, action) => {

    switch (action) {

        case 'add':

            return state + 1;

        case 'remove':

            return state - 1;

        default:

            return state;

    }

};




const App = () => {

    const [state, dispatch] = useReducer(reducer, 0);




    return (

        <React.Fragment>

            <button onClick={() => dispatch('add')}>Addvocado</button>

            <button onClick={() => dispatch('remove')}>Removocado</button>

            <div>{Array(state).fill('🥑').join(',')}</div>

        </React.Fragment>

    );

};

همانطور که می بینید در این قسمت از رشته های ساده استفاده کرده ایم.

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

function init(initialCount) {

  return {count: initialCount};

}




function reducer(state, action) {

  switch (action.type) {

    case 'increment':

      return {count: state.count + 1};

    case 'decrement':

      return {count: state.count - 1};

    case 'reset':

      return init(action.payload);

    default:

      throw new Error();

  }

}




function Counter({initialCount}) {

  const [state, dispatch] = useReducer(reducer, initialCount, init);

  return (

    <>

      Count: {state.count}

      <button

        onClick={() => dispatch({type: 'reset', payload: initialCount})}>

        Reset

      </button>

      <button onClick={() => dispatch({type: 'decrement'})}>-</button>

      <button onClick={() => dispatch({type: 'increment'})}>+</button>

    </>

  );

}

اگر کاربر روی دکمه reset کلیک کند state برنامه ریست شده و تعداد آووکادو ها به مقدار اولیه (initialCount) برمی گردد. توجه داشته باشید که initialCount به عنوان prop به این کامپوننت پاس داده شده است.

سوال نهایی این است که از useState استفاده کنیم و یا اینکه مستقیما به سراغ useReducer برویم؟ برای پاسخ به این سوال باید به state برنامه خود نگاه کنید. اگر می دانید state برنامه شما پیچیده است باید به سراغ useReducer بروید اما اگر state ساده ای دارید هیچ نیازی به پیچیده تر کردن کار نیست و همان useState کار شما را به صورت کامل انجام می دهد.

۸. هوک useContext

برای آشنایی با هوک useContext ابتدا باید با Context API در react آشنا بشوید. Context در react راهی برای پاس دادن داده ها بین کامپوننت ها بدون نیاز به پاس دادن دستی props در هر کامپوننت است. در برنامه های عادی react داده ها به صورت prop از کامپوننت پدر به کامپوننت فرزند پاس داده می شوند اما اگر نیاز باشد آن داده را به سه یا چهار کامپوننت پایین تر پاس بدهیم با کدنویسی اضافه و آزاردهنده روبرو خواهیم شد. به طور مثال:

class App extends React.Component {

  render() {

    return <Toolbar theme="dark" />;

  }

}




function Toolbar(props) {

  return (

    <div>

      <ThemedButton theme={props.theme} />

    </div>

  );

}




class ThemedButton extends React.Component {

  render() {

    return <Button theme={this.props.theme} />;

  }

}

در اینجا برای پاس دادن تم، کامپوننت Toolbar را مجبور کرده ایم که یک prop را برای آن بگیرد که اصلا خودش از آن استفاده نمی کند بلکه فقط آن را به کامپوننت فرزند خود (ThemedButton) پاس می دهد.

استفاده اصلی Context API برای داده های سراسری است (مثلا کاربر لاگین شده، تم انتخاب شده، زبان انتخاب شده و الی آخر). ما می توانیم از Context API برای مثال بالا استفاده کنیم:

const ThemeContext = React.createContext('light');




class App extends React.Component {

  render() {

    return (

      <ThemeContext.Provider value="dark">

        <Toolbar />

      </ThemeContext.Provider>

    );

  }

}




function Toolbar() {

  return (

    <div>

      <ThemedButton />

    </div>

  );

}




class ThemedButton extends React.Component {

  static contextType = ThemeContext;

  render() {

    return <Button theme={this.context} />;

  }

}

همانطور که می بینید ابتدا متدی به نام createContext را صدا زده ایم و مقدار اولیه آن را روی رشته light تنظیم کرده ایم (در بسیاری از مواقع مقدار اولیه را null می گذارند). نتیجه در متغیری به نام ThemeContext ذخیره می شود. از این به بعد  ThemeContext یک شیء Context نامیده می شود بنابراین اگر عبارت Context Object را شنیدید باید بدانید که منظور چیست.

در مرحله بعدی از نام متغیر شیء Context (متغیر ThemeContext) استفاده می کنیم تا یک کامپوننت ایجاد کنید. خصوصیت provider در این شیء این کار را برایمان انجام می دهد. هر کامپوننتی که درون این کامپوننت قرار بگیرد به شیء Context و مقدار آن دسترسی خواهد داشت. این کامپوننت یک خصوصیت ویژه به نام value را دارد که می توانید مقدار جدید را با استفاده از آن پاس بدهید (اگر مقدار اولیه light را می خواهید طبیعتا نیازی به پاس دادن چیزی نیست). من آن را روی dark گذاشته ام بنابراین مقدار ThemeContext در کامپوننت های فرزند برابر با dark خواهد بود.

حالا دیگر نیازی به پاس دادن prop به کامپوننت Toolbar نیست بلکه می توانیم به صورت مستقیم از آن استفاده کنید. چطور؟ یک خصوصیت استاتیک به نام contextType را تعریف می کنیم و مقدار آن را برابر با شیء Context می گذاریم. مسئله اینجاست که تعریف خصوصیت static تنها برای کامپوننت های کلاس محور قابل انجام است بنابراین چطور می توانیم از آن در کامپوننت های تابعی استفاده کنیم؟

هوک useContext دقیقا معادل آن خصوصیت static است و به شما اجازه می دهد که مقدار Context را دریافت کنید و همچنین به تغییرات آن گوش دهید. به این مثال توجه کنید. من نام فایل ها را به صورت کامنت در بالای کد ها قرار داده ام.

// UserContext.js

import { createContext } from 'react';




const UserContext = createContext({});

const UserProvider = UserContext.Provider;




export { UserContext, UserProvider };

اینجا جایی است که من شیء Context خودم را ساخته ام و به صورت پیش فرض مقدارش را برابر یک شیء خالی گذاشته ام. همچنین خصوصیت Provider را از آن جدا کرده و آن را در متغیری به نام UserProvider ذخیره کرده ام. از طرفی فایل dashboard.js را داریم که یک کامپوننت ساده است و فقط یک تگ H1 را برمی گرداند:

// Dashboard.js

import React from 'react';




export default () => <h1>Dashboard</h1>;

در نهایت فایل اصلی را داریم:

// useContext.js فایل




import React from 'react';

import { BrowserRouter, Route, Link } from 'react-router-dom';




import { UserProvider } from './Context';

import Dashboard from './Dashboard';

import Settings from './Settings';




export default function App() {

    const user = {

        name: 'John Doe',

        email: 'john@doe.com'

    };




    return (

        <BrowserRouter>

            <ul>

                <li><Link to="/">Dashboard</Link></li>

                <li><Link to="/settings">Settings</Link></li>

            </ul>

            <UserProvider value={user}>

                <Route path="/" exact component={Dashboard} />

                <Route path="/settings" component={Settings} />

            </UserProvider>

        </BrowserRouter>

    );

}

ما در این بخش دو route را داریم: یک مسیر برای صفحه اصلی سایت و یک مسیر برای بخش تنظیمات. همانطور که می بینید هر دو کامپوننت را درون UserProvider قرار داده ایم بنابراین هر دو به Context دسترسی دارند.

حالا برای اینکه از این هوک استفاده کنیم به فایلی به نام Dashboard.js رفته و آن را صدا می زنیم. نام گذاری این فایل ها بر اساس سلیقه من بوده است و طبیعتا شما می توانید ساختار پروژه خود را تغییر بدهید:

import React, { useContext } from 'react';




import { UserContext } from './Context';




export default () => {

    const user = useContext(UserContext);




    return (

        <React.Fragment>

            <h1>Dashboard</h1>

            <span>Logged in as {user.name} ({user.email})</span>

        </React.Fragment>

    );

};

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

اگر بخواهید این پروژه را پیشرفته تر کنید باید useState را به useContext پاس بدهیم! چطور؟ به مثال زیر توجه کنید:

// useContext.js فایل




import React, { useState } from 'react';




export default function App() {

    const [state, setState] = useState(user);




    return (

        // بقیه کد ها

        <UserProvider value={[state, setState]}>

            <Route path="/" exact component={Dashboard} />

            <Route path="/settings" component={Settings} />

        </UserProvider>

    );

}

حالا اگر بخواهیم در بخشی از برنامه state را ویرایش کنیم می توانیم به راحتی این کار را انجام بدهیم. مثلا من این کار را در فایلی فرضی به نام settings.js انجام می دهم:

// Settings.js فایل




import React, { useContext } from 'react';




import { UserContext } from './Context';




export default () => {

    const [user, setUser] = useContext(UserContext);




    const changeEmail = () => {

        setUser({

            ...user,

            email: 'john@update.com'

        });

    };




    return (

        <React.Fragment>

            <h1>Settings</h1>

            <span>Logged in as {user.name} ({user.email})</span>

            <br />

            <button onClick={changeEmail}>Change email</button>

        </React.Fragment>

    );

};

۹. هوک useImperativeHandle

زمانی که با استفاده از ref یک ارجاع را به کامپوننت پدر ارسال می کنید می توانید از هوک useImperativeHandle برای ویرایش آن استفاده کنید. این هوک ساختار زیر را دارد:

useImperativeHandle(ref, createHandle, [deps])
  • ref همان ref ای است که استفاده کرده اید.
  • createHandle تابعی است که روی ref اجرا می شود.
  • deps آرایه ای وابستگی ها است که قبلا در مورد آن صحبت کرده ایم (مانند useEffect).

باید خاطر نشان کرد که استفاده از ref به طور کل توسط تیم react پیشنهاد نمی شود بلکه چندین بار در documentation اصلی در این باره توضیح داده اند بنابراین فقط در شرایط خاص از آن استفاده کنید.

برای استفاده از این هوک باید از forwardRef استفاده کنیم بنابراین انتظار دارم با آن آشنا باشید. به صورت خلاصه می گویم که forwardRef کامپوننتی می سازد که ref دریافت شده را به کامپوننت بعدی در درخت کامپوننت ها ارسال می کند. با این حساب می توانیم چنین مثالی را برایش داشته باشیم:

function FancyInput(props, ref) {

  const inputRef = useRef();

  useImperativeHandle(ref, () => ({

    focus: () => {

      inputRef.current.focus();

    }

  }));

  return <input ref={inputRef} ... />;

}

FancyInput = forwardRef(FancyInput);

ما در این کد یک کامپوننت به نام FancyInput داریم که یک input را برمی گرداند و از ref استفاده می کند. هدف از این مثال چیست؟ با این کار کامپوننت پدری که کامپوننت FancyInput را نمایش می دهد می تواند متد focus را صدا بزند. این مسئله بدون حضور forwardRef ممکن نبود.

۱۰. هوک useLayoutEffect

این هوک دقیقا مانند useEffect است با این تفاوت که پس از بارگذاری عناصر DOM و تغییرات آن ها به صورت همگام (synchronous) اجرا می شود. بنابراین تمام re-render ها به صورت همگام خواهند بود. این هوک فقط برای شرایط خاصی تعریف شده است که بخواهیم کد هایی را قبل از render شدن صفحه اجرا کنیم. طبیعتا چنین هوکی مسدود کننده render عناصر است بنابراین با استفاده از آن سرعت برنامه خود را کم می کنید. documentation رسمی react توصیه می کند که تا حد ممکن از useEffect استفاده کنید و فقط زمانی از useLayoutEffect استفاده کنید که useEffect نتیجه مطلوب را برایتان نداشته است.

به طور مثال اگر از Server-Side Rendering استفاده می کنید (فریم ورک هایی مانند next.js) و درون کامپوننت هایتان useLayoutEffect وجود داشته باشد یک هشدار از طرف react دریافت می کنید. چرا؟ به دلیل اینکه useLayoutEffect باعث بلوک شدن نمایش کامپوننت ها می شود. راه حل این است که به جای useLayoutEffect از useEffect استفاده کرده و یا بارگذاری کامپوننت مورد نظر را آنقدر به تعویق بیندازید تا بخش کلاینت به طور کامل render شود.

۱۱. هوک useDebugValue

این هوک به شما اجازه می دهد برای هوک های custom (هوک هایی که مربوط به react نیستند بلکه خودتان آن ها را نوشته اید) در افزونه React DevTools نام انتخاب کنید. حالا که صحبت از هوک های custom شد بهتر است به سراغ آن ها نیز برویم.

۱۲. هوک های شخصی

هوک های شخصی یا custom هوک هایی هستند که توسط تیم react ارائه نشده اند بلکه توسط شما یا برنامه نویسان دیگر نوشته شده اند. بیایید یک مسئله ساده برای تمام برنامه های react را در نظر بگیریم: نمایش یک spinner (آیکون چرخان نشان دهنده حالت loading) در هنگام بارگذاری صفحات. در بسیاری از برنامه های react ای باید روشی داشته باشیم که به کاربر بگوییم صفحه در حال بارگذاری است بنابراین کاربر باید صبر کند.

من نام این هوک را useFetch می گذارم تا به استاندارد های نام گذاری نیز پایبند باشیم. قبل از اینکه بخواهیم به سراغ نوشتن این هوک برویم، دوست دارم نحوه استفاده از آن را به شما نشان بدهم:

import React from 'react';




import useFetch from './useFetch';




const App = () => {

    const [post, loading] = useFetch("https://jsonplaceholder.typicode.com/posts/1");




    return (

        <div className="App">

            <div>{loading ? "loading..." : post}</div>

        </div>

    );

};

همانطور که می بینید هوک ما قرار است یک URL را دریافت کند و دو مقدار را به ما بدهد:

  • Post: داده دریافت شده از URLاست. ما درخواست را به سرور های معروف jsonplaceholder ارسال کرده ایم تا یک پست را دریافت کنیم. به همین خاطر است که نامش را post گذاشته ام.
  • loading: یک متغیر که نشان می دهد آیا در حال دریافت داده ها هستیم یا خیر. این مقدار از نوع boolean خواهد بود.

من بر اساس true بودن متغیر loading پست دریافت شده را نشان می دهم. ما می خواهیم بدین شکل از هوک خودمان استفاده کنیم بنابراین باید آن را به همین شکل تعریف می کنیم. اگر بخواهم به زبان ساده توضیح بدهم، یک هوک فقط یک تابع جاوا اسکریپتی است که نامش با use شروع می شود و ممکن است هوک های دیگر را نیز صدا بزند. با این حساب ساخت هوکی که توضیح دادم اصلا سخت نیست چرا که فقط نیاز به تعریف یک تابع داریم:

import { useState, useEffect } from 'react';




export default url => {

    const [state, setState] = useState([null, false]);




    useEffect(() => {

        setState([null, true]);




        (async () => {

            const data = await fetch(url).then(res => res.json());




            setState([data.body, false]);

        })();

    }, [url]);




    return state;

};

ما درون هوک خودمان از دو هوک دیگر (useState و useEffect) استفاده کرده ایم. برای این کار ابتدا useState را صدا زده و یک آرایه ساده با دو عضو را به آن داده ایم که state اولیه برنامه ما خواهند بود. عضو اول آرایه داده هایمان است که فعلا null هستند و عضو دوم نیز حالت loading را نشان می دهد که باید در ابتدا false باشد. حالا درون useEffect یک درخواست را به API مورد نظر ارسال می کنیم اما قبل از ارسال درخواست باید state برنامه را روی حالت loading قرار بدهیم بنابراین عضو دوم آرایه را روی true تنظیم می کنم.

در ضمن شاید با ساختار تابع من در useEffect آشنا نباشید. در جاوا اسکریپت به این توابع self-invoking function یا توابع خود فراخوان می گوییم چرا که خودشان به صورت خودکار فراخوانی شده و نیازی به صدا زدن ندارند. پس از آنکه داده ها را دریافت کرده ایم دوباره با setState داده را از حالت loading در آورده ایم و داده های دریافت شده از API مورد نظر را نیز به عنوان آرگومان اول پاس داده ایم. در ضمن از آنجایی که url ما تنها وابستگی useEffect است، آن را به عنوان آرگومان دوم به useEffect پاس داده ایم. در نهایت نیز state را برگردانده ایم.

به همین سادگی یک هوک برای خودمان تعریف کرده ایم!


منبع: وب سایت webtips

نویسنده شوید
دیدگاه‌های شما

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