你應該會喜歡的5個自定義 Hook
文本已經過原作者 Grégory D'Angelo 授權翻譯。
React hooks

React hooks 已經在16.8版本引入到庫中。它允許我們在函數組件中使用狀態(tài)和其他React特性,這樣我們甚至不需要再編寫類組件。
實際上,Hooks 遠不止于此。
Hooks 可以將組件內的邏輯組織成可重用的獨立單元。
Hooks 非常適合 React 組件模型和構建應用程序的新方法。Hooks 可以覆蓋類的所有用例,同時在整個應用程序中提供更多的提取、測試和重用代碼的靈活性。
構建自己的自定義React鉤子,可以輕松地在應用程序的所有組件甚至不同應用程序之間共享特性,這樣我們就不必重復自己的工作,從而提高構建React應用程序的效率。
現在,來看看我在開發(fā)中最常用的5個自定義鉤子,并頭開始重新創(chuàng)建它們,這樣你就能夠真正理解它們的工作方式,并確切地了解如何使用它們來提高生產率和加快開發(fā)過程。
我們直接開始創(chuàng)建我們的第一個自定義React Hooks。
useFetch
獲取數據是我每次創(chuàng)建React應用時都會做的事情。我甚至在一個應用程序中進行了好多個這樣的重復獲取。
不管我們選擇哪種方式來獲取數據,Axios、Fetch API,還是其他,我們很有可能在React組件序中一次又一次地編寫相同的代碼。
因此,我們看看如何構建一個簡單但有用的自定義 Hook,以便在需要在應用程序內部獲取數據時調用該 Hook。
okk,這個 Hook 我們叫它 useFetch。
這個 Hook 接受兩個參數,一個是獲取數據所需查詢的URL,另一個是表示要應用于請求的選項的對象。
- import { useState, useEffect } from 'react';
- const useFetch = (url = '', options = null) => {};
- export default useFetch;
獲取數據是一個副作用。因此,我們應該使用useEffect Hook 來執(zhí)行查詢。
在本例中,我們使用 Fetch API來發(fā)出請求。我們會傳遞URL和 options。一旦 Promise 被解決,我們就通過解析響應體來檢索數據。為此,我們使用json()方法。
然后,我們只需要將它存儲在一個React state 變量中。
- import { useState, useEffect } from 'react';
- const useFetch = (url = '', options = null) => {
- const [data, setData] = useState(null);
- useEffect(() => {
- fetch(url, options)
- .then(res => res.json())
- .then(data => setData(data));
- }, [url, options]);
- };
- export default useFetch;
這里,我們還需要處理網絡錯誤,以防我們的請求出錯。所以我們要用另一個 state 變量來存儲錯誤。這樣我們就能從 Hook 中返回它并能夠判斷是否發(fā)生了錯誤。
- import { useState, useEffect } from 'react';
- const useFetch = (url = '', options = null) => {
- const [data, setData] = useState(null);
- const [error, setError] = useState(null);
- useEffect(() => {
- fetch(url, options)
- .then(res => res.json())
- .then(data => {
- if (isMounted) {
- setData(data);
- setError(null);
- }
- })
- .catch(error => {
- if (isMounted) {
- setError(error);
- setData(null);
- }
- });
- }, [url, options]);
- };
- export default useFetch;
useFetch返回一個對象,其中包含從URL中獲取的數據,如果發(fā)生了任何錯誤,則返回錯誤。
- return { error, data };
最后,向用戶表明異步請求的狀態(tài)通常是一個好做法,比如在呈現結果之前顯示 loading。
因此,我們添加第三個 state 變量來跟蹤請求的狀態(tài)。在請求之前,將loading設置為true,并在請求之后完成后設置為false。
- const useFetch = (url = '', options = null) => {
- const [data, setData] = useState(null);
- const [error, setError] = useState(null);
- const [loading, setLoading] = useState(false);
- useEffect(() => {
- setLoading(true);
- fetch(url, options)
- .then(res => res.json())
- .then(data => {
- setData(data);
- setError(null);
- })
- .catch(error => {
- setError(error);
- setData(null);
- })
- .finally(() => setLoading(false));
- }, [url, options]);
- return { error, data };
- };
現在,我們可以返回 loading 變量,以便在請求運行時在組件中使用它來呈現一個 loading,方便用戶知道我們正在獲取他們所請求的數據。
- return { loading, error, data };
在使用 userFetch 之前,我們還有一件事。
我們需要檢查使用我們 Hook 的組件是否仍然被掛載,以更新我們的狀態(tài)變量。否則,會有內存泄漏。
- import { useState, useEffect } from 'react';
- const useFetch = (url = '', options = null) => {
- const [data, setData] = useState(null);
- const [error, setError] = useState(null);
- const [loading, setLoading] = useState(false);
- useEffect(() => {
- let isMounted = true;
- setLoading(true);
- fetch(url, options)
- .then(res => res.json())
- .then(data => {
- if (isMounted) {
- setData(data);
- setError(null);
- }
- })
- .catch(error => {
- if (isMounted) {
- setError(error);
- setData(null);
- }
- })
- .finally(() => isMounted && setLoading(false));
- return () => (isMounted = false);
- }, [url, options]);
- return { loading, error, data };
- };
- export default useFetch;
接下就是怎么用了?
我們只需要傳遞我們想要檢索的資源的URL。從那里,我們得到一個對象,我們可以使用它來渲染我們的應用程序。
- import useFetch from './useFetch';
- const App = () => {
- const { loading, error, data = [] } = useFetch(
- 'https://hn.algolia.com/api/v1/search?query=react'
- );
- if (error) return <p>Error!</p>;
- if (loading) return <p>Loading...</p>;
- return (
- <div>
- <ul>
- {data?.hits?.map(item => (
- <li key={item.objectID}>
- <a href={item.url}>{item.title}</a>
- </li>
- ))}
- </ul>
- </div>
- );
- };
useEventListener
這個 Hook 負責在組件內部設置和清理事件監(jiān)聽器。
這樣,我們就不需要每次添加事件監(jiān)聽器,做重復的工作。
這個函數有幾個參數,eventType 事件類型,listener 監(jiān)聽函數,target 監(jiān)聽對象,options 可選參數。
- import { useEffect, useRef } from 'react';
- const useEventListener = (
- eventType = '',
- listener = () => null,
- target = null,
- options = null
- ) => {};
- export default useEventListener;
與前一個 Hook 一樣,用 useEffect 來添加一個事件監(jiān)聽器。首先,我們需要確保target 是否支持addEventListener方法。否則,我們什么也不做。
- import { useEffect, useRef } from 'react';
- const useEventListener = (
- eventType = '',
- listener = () => null,
- target = null,
- options = null
- ) => {
- useEffect(() => {
- if (!target?.addEventListener) return;
- }, [target]);
- };
- export default useEventListener;
然后,我們可以添加實際的事件監(jiān)聽器并在卸載函數中刪除它。
- import { useEffect, useRef } from 'react';
- const useEventListener = (
- eventType = '',
- listener = () => null,
- target = null,
- options = null
- ) => {
- useEffect(() => {
- if (!target?.addEventListener) return;
- target.addEventListener(eventType, listener, options);
- return () => {
- target.removeEventListener(eventType, listener, options);
- };
- }, [eventType, target, options, listener]);
- };
- export default useEventListener;
實際上,我們也會使用一個引用對象來存儲和持久化監(jiān)聽器函數。只有當監(jiān)聽器函數發(fā)生變化并在事件監(jiān)聽器方法中使用該引用時,我們才會更新該引用。
- import { useEffect, useRef } from 'react';
- const useEventListener = (
- eventType = '',
- listener = () => null,
- target = null,
- options = null
- ) => {
- const savedListener = useRef();
- useEffect(() => {
- savedListener.current = listener;
- }, [listener]);
- useEffect(() => {
- if (!target?.addEventListener) return;
- const eventListener = event => savedListener.current(event);
- target.addEventListener(eventType, eventListener, options);
- return () => {
- target.removeEventListener(eventType, eventListener, options);
- };
- }, [eventType, target, options]);
- };
- export default useEventListener;
我們不需要從此 Hook 返回任何內容,因為我們只是偵聽事件并運行處理程序函數傳入作為參數。
現在,很容易將事件偵聽器添加到我們的組件(例如以下組件)中,以檢測DOM元素外部的點擊。如果用戶單擊對話框組件,則在此處關閉對話框組件。
- import { useRef } from 'react';
- import ReactDOM from 'react-dom';
- import { useEventListener } from './hooks';
- const Dialog = ({ show = false, onClose = () => null }) => {
- const dialogRef = useRef();
- // Event listener to close dialog on click outside element
- useEventListener(
- 'mousedown',
- event => {
- if (event.defaultPrevented) {
- return; // Do nothing if the event was already processed
- }
- if (dialogRef.current && !dialogRef.current.contains(event.target)) {
- console.log('Click outside detected -> closing dialog...');
- onClose();
- }
- },
- window
- );
- return show
- ? ReactDOM.createPortal(
- <div className="fixed inset-0 z-9999 flex items-center justify-center p-4 md:p-12 bg-blurred">
- <div
- className="relative bg-white rounded-md shadow-card max-h-full max-w-screen-sm w-full animate-zoom-in px-6 py-20"
- ref={dialogRef}
- >
- <p className="text-center font-semibold text-4xl">
- What's up{' '}
- <span className="text-white bg-red-500 py-1 px-3 rounded-md mr-1">
- YouTube
- </span>
- ?
- </p>
- </div>
- </div>,
- document.body
- )
- : null;
- };
- export default Dialog;
useLocalStorage
這個 Hook 主要有兩個參數,一個是 key,一個是 value。
- import { useState } from 'react';
- const useLocalStorage = (key = '', initialValue = '') => {};
- export default useLocalStorage;
然后,返回一個數組,類似于使用 useState 獲得的數組。因此,此數組將包含有狀態(tài)值和在將其持久存儲在localStorage 中時對其進行更新的函數。
首先,我們創(chuàng)建將與 localStorage 同步的React狀態(tài)變量。
- import { useState } from 'react';
- const useLocalStorage = (key = '', initialValue = '') => {
- const [state, setState] = useState(() => {
- try {
- const item = window.localStorage.getItem(key);
- return item ? JSON.parse(item) : initialValue;
- } catch (error) {
- console.log(error);
- return initialValue;
- }
- });
- };
- export default useLocalStorage;
在這里,我們使用惰性初始化來讀取 localStorage 以獲取鍵的值,如果找到該值,則解析該值,否則返回傳入的initialValue。
如果在讀取 localStorage 時出現錯誤,我們只記錄一個錯誤并返回初始值。
最后,我們需要創(chuàng)建 update 函數來返回它將在localStorage 中存儲任何狀態(tài)的更新,而不是使用useState 返回的默認更新。
- import { useState } from 'react';
- const useLocalStorage = (key = '', initialValue = '') => {
- const [state, setState] = useState(() => {
- try {
- const item = window.localStorage.getItem(key);
- return item ? JSON.parse(item) : initialValue;
- } catch (error) {
- return initialValue;
- }
- });
- const setLocalStorageState = newState => {
- try {
- const newStateValue =
- typeof newState === 'function' ? newState(state) : newState;
- setState(newStateValue);
- window.localStorage.setItem(key, JSON.stringify(newStateValue));
- } catch (error) {
- console.error(`Unable to store new value for ${key} in localStorage.`);
- }
- };
- return [state, setLocalStorageState];
- };
- export default useLocalStorage;
此函數同時更新React狀態(tài)和 localStorage 中的相應鍵/值。這里,我們還可以支持函數更新,例如常規(guī)的useState hook。
最后,我們返回狀態(tài)值和我們的自定義更新函數。
現在可以使用useLocalStorage hook 將組件中的任何數據持久化到localStorage中。
- import { useLocalStorage } from './hooks';
- const defaultSettings = {
- notifications: 'weekly',
- };
- function App() {
- const [appSettings, setAppSettings] = useLocalStorage(
- 'app-settings',
- defaultSettings
- );
- return (
- <div className="h-full w-full flex flex-col justify-center items-center">
- <div className="flex items-center mb-8">
- <p className="font-medium text-lg mr-4">Your application's settings:</p>
- <select
- value={appSettings.notifications}
- onChange={e =>
- setAppSettings(settings => ({
- ...settings,
- notifications: e.target.value,
- }))
- }
- className="border border-gray-900 rounded py-2 px-4 "
- >
- <option value="daily">daily</option>
- <option value="weekly">weekly</option>
- <option value="monthly">monthly</option>
- </select>
- </div>
- <button
- onClick={() => setAppSettings(defaultSettings)}
- className="rounded-md shadow-md py-2 px-6 bg-red-500 text-white uppercase font-medium tracking-wide text-sm leading-8"
- >
- Reset settings
- </button>
- </div>
- );
- }
- export default App;
useMediaQuery
這個 Hook 幫助我們在功能組件中以編程方式測試和監(jiān)控媒體查詢。這是非常有用的,例如,當你需要渲染不同的UI取決于設備的類型或特定的特征。
我們的 Hook 接受3個參數:
- 首先,對應媒體查詢的字符串數組
- 然后,以與前一個數組相同的順序匹配這些媒體查詢的值數組
- 最后,如果沒有匹配的媒體查詢,則使用默認值
- import { useState, useCallback, useEffect } from 'react';
- const useMediaQuery = (queries = [], values = [], defaultValue) => {};
- export default useMediaQuery;
我們在這個 Hook 中做的第一件事是為每個匹配的媒體查詢構建一個媒體查詢列表。使用這個數組通過匹配媒體查詢來獲得相應的值。
- import { useState, useCallback, useEffect } from 'react';
- const useMediaQuery = (queries = [], values = [], defaultValue) => {
- const mediaQueryList = queries.map(q => window.matchMedia(q));
- };
- export default useMediaQuery;
為此,我們創(chuàng)建了一個包裝在useCallback 中的回調函數。檢索列表中第一個匹配的媒體查詢的值,如果沒有匹配則返回默認值。
- import { useState, useCallback, useEffect } from 'react';
- const useMediaQuery = (queries = [], values = [], defaultValue) => {
- const mediaQueryList = queries.map(q => window.matchMedia(q));
- const getValue = useCallback(() => {
- const index = mediaQueryList.findIndex(mql => mql.matches);
- return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
- }, [mediaQueryList, values, defaultValue]);
- };
- export default useMediaQuery;
然后,我們創(chuàng)建一個React狀態(tài)來存儲匹配的值,并使用上面定義的函數來初始化它。
- import { useState, useCallback, useEffect } from 'react';
- const useMediaQuery = (queries = [], values = [], defaultValue) => {
- const mediaQueryList = queries.map(q => window.matchMedia(q));
- const getValue = useCallback(() => {
- const index = mediaQueryList.findIndex(mql => mql.matches);
- return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
- }, [mediaQueryList, values, defaultValue]);
- const [value, setValue] = useState(getValue);
- };
- export default useMediaQuery;
最后,我們在 useEffect 中添加一個事件監(jiān)聽器來監(jiān)聽每個媒體查詢的更改。當發(fā)生變化時,我們運行更新函數。
- mport { useState, useCallback, useEffect } from 'react';
- const useMediaQuery = (queries = [], values = [], defaultValue) => {
- const mediaQueryList = queries.map(q => window.matchMedia(q));
- const getValue = useCallback(() => {
- const index = mediaQueryList.findIndex(mql => mql.matches);
- return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
- }, [mediaQueryList, values, defaultValue]);
- const [value, setValue] = useState(getValue);
- useEffect(() => {
- const handler = () => setValue(getValue);
- mediaQueryList.forEach(mql => mql.addEventListener('change', handler));
- return () =>
- mediaQueryList.forEach(mql => mql.removeEventListener('change', handler));
- }, [getValue, mediaQueryList]);
- return value;
- };
- export default useMediaQuery;
我最近使用的一個簡單的例子是添加一個媒體查詢來檢查設備是否允許用戶懸停在元素上。這樣,如果用戶可以懸停或應用基本樣式,我就可以添加特定的不透明樣式。
- import { useMediaQuery } from './hooks';
- function App() {
- const canHover = useMediaQuery(
- // Media queries
- ['(hover: hover)'],
- // Values corresponding to the above media queries by array index
- [true],
- // Default value
- false
- );
- const canHoverClass = 'opacity-0 hover:opacity-100 transition-opacity';
- const defaultClass = 'opacity-100';
- return (
- <div className={canHover ? canHoverClass : defaultClass}>Hover me!</div>
- );
- }
- export default App;
useDarkMode
這個是我的最愛。它能輕松快速地將暗模式功能應用于任何React應用程序。

這個 Hook 主要按需啟用和禁用暗模式,將當前狀態(tài)存儲在localStorage 中。
為此,我們將使用我們剛剛構建的兩個鉤子:useMediaQuery和useLocalStorage。
然后,使用“ useLocalStorage”,我們可以在localStorage中初始化,存儲和保留當前狀態(tài)(暗或亮模式)。
- import { useEffect } from 'react';
- import useMediaQuery from './useMediaQuery';
- import useLocalStorage from './useLocalStorage';
- const useDarkMode = () => {
- const preferDarkMode = useMediaQuery(
- ['(prefers-color-scheme: dark)'],
- [true],
- false
- );
- };
- export default useDarkMode;
最后一部分是觸發(fā)副作用,以向document.body元素添加或刪除dark類。這樣,我們可以簡單地將dark樣式應用于我們的應用程序。
- import { useEffect } from 'react';
- import useMediaQuery from './useMediaQuery';
- import useLocalStorage from './useLocalStorage';
- const useDarkMode = () => {
- const preferDarkMode = useMediaQuery(
- ['(prefers-color-scheme: dark)'],
- [true],
- false
- );
- const [enabled, setEnabled] = useLocalStorage('dark-mode', preferDarkMode);
- useEffect(() => {
- if (enabled) {
- document.body.classList.add('dark');
- } else {
- document.body.classList.remove('dark');
- }
- }, [enabled]);
- return [enabled, setEnabled];
- };
- export default useDarkMode;
~完,我是小智,我要去刷碗了。
作者:Grégory D'Angelo 譯者:前端小智 來源: dev原文:https://dev.to/alterclass/5-react-custom-hooks-you-should-start-using-explained-5d18