你應該會喜歡的5個自定義 Hook

做者:Grégory D'Angelo
譯者:前端小智
來源: dev
點贊再看,微信搜索 大遷世界,B站關注 前端小智這個沒有大廠背景,但有着一股向上積極心態人。本文 GitHub https://github.com/qq44924588... 上已經收錄,文章的已分類,也整理了不少個人文檔,和教程資料。

最近開源了一個 Vue 組件,還不夠完善,歡迎你們來一塊兒完善它,也但願你們能給個 star 支持一下,謝謝各位了。前端

github 地址:https://github.com/qq44924588...vue

React hooks

image.png

React hooks 已經在16.8版本引入到庫中。它容許咱們在函數組件中使用狀態和其餘React特性,這樣咱們甚至不須要再編寫類組件。react

實際上,Hooks 遠不止於此。ios

Hooks 能夠將組件內的邏輯組織成可重用的獨立單元。git

Hooks 很是適合 React 組件模型和構建應用程序的新方法。Hooks 能夠覆蓋類的全部用例,同時在整個應用程序中提供更多的提取、測試和重用代碼的靈活性。github

構建本身的自定義React鉤子,能夠輕鬆地在應用程序的全部組件甚至不一樣應用程序之間共享特性,這樣咱們就沒必要重複本身的工做,從而提升構建React應用程序的效率。面試

如今,來看看我在開發中最經常使用的 5 個自定義鉤子,並頭開始從新建立它們,這樣你就可以真正理解它們的工做方式,並確切地瞭解如何使用它們來提升生產率和加快開發過程。編程

咱們直接開始建立咱們的第一個自定義React Hooks。json

useFetch

獲取數據是我每次建立React應用時都會作的事情。我甚至在一個應用程序中進行了好多個這樣的重複獲取。api

無論咱們選擇哪一種方式來獲取數據,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 來執行查詢。

在本例中,咱們使用 Fetch API來發出請求。咱們會傳遞URLoptions。一旦 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 中返回它並可以判斷是否發生了錯誤。

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中獲取的數據,若是發生了任何錯誤,則返回錯誤。

return { error, data };

最後,向用戶代表異步請求的狀態一般是一個好作法,好比在呈現結果以前顯示 loading。

所以,咱們添加第三個 state 變量來跟蹤請求的狀態。在請求以前,將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 的組件是否仍然被掛載,以更新咱們的狀態變量。不然,會有內存泄漏。

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 負責在組件內部設置和清理事件監聽器。

這樣,咱們就不須要每次添加事件監聽器,作重複的工做。

這個函數有幾個參數,eventType 事件類型,listener 監聽函數,target 監聽對象,options 可選參數。

import { useEffect, useRef } from 'react';

const useEventListener = (
  eventType = '',
  listener = () => null,
  target = null,
  options = null
) => {};

export default useEventListener;

與前一個 Hook 同樣,用 useEffect 來添加一個事件監聽器。首先,咱們須要確保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;

而後,咱們能夠添加實際的事件監聽器並在卸載函數中刪除它。

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;

實際上,咱們也會使用一個引用對象來存儲和持久化監聽器函數。只有當監聽器函數發生變化並在事件監聽器方法中使用該引用時,咱們纔會更新該引用。

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 得到的數組。 所以,此數組將包含有狀態值和在將其持久存儲在localStorage 中時對其進行更新的函數。

首先,咱們建立將與 localStorage 同步的React狀態變量。

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 時出現錯誤,咱們只記錄一個錯誤並返回初始值。

最後,咱們須要建立 update 函數來返回它將在localStorage 中存儲任何狀態的更新,而不是使用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狀態和 localStorage 中的相應鍵/值。 這裏,咱們還能夠支持函數更新,例如常規的useState hook。

最後,咱們返回狀態值和咱們的自定義更新函數。

如今可使用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 幫助咱們在功能組件中以編程方式測試和監控媒體查詢。這是很是有用的,例如,當你須要渲染不一樣的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;

爲此,咱們建立了一個包裝在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;

而後,咱們建立一個React狀態來存儲匹配的值,並使用上面定義的函數來初始化它。

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 中添加一個事件監聽器來監聽每一個媒體查詢的更改。當發生變化時,咱們運行更新函數。

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應用程序。

image

這個 Hook 主要按需啓用和禁用暗模式,將當前狀態存儲在localStorage 中。

爲此,咱們將使用咱們剛剛構建的兩個鉤子:useMediaQueryuseLocalStorage

而後,使用「 useLocalStorage」,咱們能夠在localStorage中初始化,存儲和保留當前狀態(暗或亮模式)。

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;

最後一部分是觸發反作用,以向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;

~完,我是小智,我要去刷碗了。


代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

原文:https://dev.to/alterclass/5-r...

交流

文章每週持續更新,能夠微信搜索「 大遷世界 」第一時間閱讀和催更(比博客早一到兩篇喲),本文 GitHub https://github.com/qq449245884/xiaozhi 已經收錄,整理了不少個人文檔,歡迎Star和完善,你們面試能夠參照考點複習,另外關注公衆號,後臺回覆福利,便可看到福利,你懂的。

相關文章
相關標籤/搜索