1. 程式人生 > 其它 >自定義Hooks:四個典型的使用場景

自定義Hooks:四個典型的使用場景

一、如何用好hook

  要用好 React Hooks,很重要的一點,就是要能夠從 Hooks 的角度去思考問題。要做到這一點其實也不難,就是在遇到一個功能開發的需求時,首先問自己一個問題:這個功能中的哪些邏輯可以抽出來成為獨立的 Hooks?

  這樣問的目的,是為了讓我們儘可能的吧業務陸奧及拆分成獨立的Hooks,這樣有助於實現程式碼的模組化和解耦,同時也方便後面的維護。hooks的連個核心的優點:

1.是方便進行邏輯複用

2.是幫助關注分離

 如何建立自定義Hooks

自定義Hooks在形式上其實非常簡單,就是宣告一個名字以use開頭的函式,比如useCounter。這個函式在形式上和普通函式沒有區別,你可以傳遞任意引數給這個Hooks。但是要注意,Hooks和普通函式在語義化上是由區別的,就在於函式沒有用到其他Hooks。什麼意思呢?就是說如果你建立了一個 useXXX 的函式,但是內部並沒有用任何其它 Hooks,那麼這個函式就不是一個 Hook,而只是一個普通的函式。但是如果用了其它 Hooks ,那麼它就是一個 Hook。

  舉一個簡單的例子,一個簡單計數器的實現,當時把業務邏輯都寫在了函式元件內部,但其實是可以把業務邏輯提取出來成為一個 Hook。比如下面的程式碼:

import { useState, useCallback }from 'react';
 
function useCounter() {
  // 定義 count 這個 state 用於儲存當前數值
  const [count, setCount] = useState(0);
  // 實現加 1 的操作
  const increment = useCallback(() => setCount(count + 1), [count]);
  
// 實現減 1 的操作 const decrement = useCallback(() => setCount(count - 1), [count]); // 重置計數器 const reset = useCallback(() => setCount(0), []); // 將業務邏輯的操作 export 出去供呼叫者使用 return { count, increment, decrement, reset }; }

有了這個 Hook,我們就可以在元件中使用它,比如下面的程式碼:

import React from 'react';

function Counter() {
  
// 呼叫自定義 Hook const { count, increment, decrement, reset } = useCounter(); // 渲染 UI return ( <div> <button onClick={decrement}> - </button> <p>{count}</p> <button onClick={increment}> + </button> <button onClick={reset}> reset </button> </div> ); }

在這段程式碼中,我們把原來在函式元件中實現的邏輯提取了出來,成為一個單獨的 Hook,一方面能讓這個邏輯得到重用,另外一方面也能讓程式碼更加語義化,並且易於理解和維護。

從這個例子,我們可以看出自定義Hooks的兩個特點:

1.名字一定是以use開頭的函式,這樣react才能知道這個函式是一個Hooks;

2.函式內部一定呼叫了其他的Hooks,可以是內建的Hooks,也可以是自定義Hooks。這樣才能夠讓元件重新整理,或者去產生副作用。

  封裝通用邏輯:useAsync

在元件的開發過程中,有一些常用的通用邏輯。過去可能應為邏輯重用比較繁瑣,而經常在每個元件中去自己實現,造成維護的困難。但現有了Hooks,就可以將更多的通用邏輯通過Hooks的形式進行封裝,方便被不同的元件重用。

比如說,在日常UI的開發中,有一個最常b見的需求:發起非同步請求獲取資料並顯示在介面上。在這個過程中,我們不僅要關心請求正常返回時UI如何戰術資料;還需要處理請求出錯,以及慣出loding狀態在UI上如何顯示。

  我們可以重新看下在第 1 講中看到的非同步請求的例子,從 Server 端獲取使用者列表,並顯示在介面上:

import React from "react";

export default function UserList() {
  // 使用三個 state 分別儲存使用者列表,loading 狀態和錯誤狀態
  const [users, setUsers] = React.useState([]);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);

  // 定義獲取使用者的回撥函式
  const fetchUsers = async () => {
    setLoading(true);
    try {
      const res = await fetch("https://reqres.in/api/users/");
      const json = await res.json();
      // 請求成功後將使用者資料放入 state
      setUsers(json.data);
    } catch (err) {
      // 請求失敗將錯誤狀態放入 state
      setError(err);
    }
    setLoading(false);
  };

  return (
    <div className="user-list">
      <button onClick={fetchUsers} disabled={loading}>
        {loading ? "Loading..." : "Show Users"}
      </button>
      {error && 
        <div style={{ color: "red" }}>Failed: {String(error)}</div>
      }
      <br />
      <ul>
        {users && users.length > 0 &&
          users.map((user) => {
            return <li key={user.id}>{user.first_name}</li>;
          })}
      </ul>
    </div>
  );
}

在這裡,我們定義了users、loading和error三個狀態。如果我們在非同步請求的不同階段去設定不同的轉檯,這樣Ui最終能夠根據這些狀態展示出來,在每個需要非同步請求的元件中,其實都需要重複的邏輯。

事實上,在處理這類請求的時候,模式都是類似的,通常都會遵循下面步驟:

1.建立data,loding,error這3個state;

2.請求發出後,設定loading state為true;

3.請求成功後,將返回的資料放到某個state中,並將loading state設定false;

4.請求失敗後,設定error state為true,並將loading state設為false。

最後,基於data、loading、error這3個state的資料,ui就可以正確的顯示資料,或者loading、error這些反饋給客戶了。

所以,通過建立一個自定義Hook,可以很好的將這樣的邏輯提取出來,成為一個可重用的模組。比如程式碼可以這樣實現:

import React from "react";

export default function UserList() {
  // 使用三個 state 分別儲存使用者列表,loading 狀態和錯誤狀態
  const [users, setUsers] = React.useState([]);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);

  // 定義獲取使用者的回撥函式
  const fetchUsers = async () => {
    setLoading(true);
    try {
      const res = await fetch("https://reqres.in/api/users/");
      const json = await res.json();
      // 請求成功後將使用者資料放入 state
      setUsers(json.data);
    } catch (err) {
      // 請求失敗將錯誤狀態放入 state
      setError(err);
    }
    setLoading(false);
  };

  return (
    <div className="user-list">
      <button onClick={fetchUsers} disabled={loading}>
        {loading ? "Loading..." : "Show Users"}
      </button>
      {error && 
        <div style={{ color: "red" }}>Failed: {String(error)}</div>
      }
      <br />
      <ul>
        {users && users.length > 0 &&
          users.map((user) => {
            return <li key={user.id}>{user.first_name}</li>;
          })}
      </ul>
    </div>
  );
}

那麼有了這個 Hook,我們在元件中就只需要關心與業務邏輯相關的部分。比如程式碼可以簡化成這樣的形式:

import React from "react";
import useAsync from './useAsync';

export default function UserList() {
  // 通過 useAsync 這個函式,只需要提供非同步邏輯的實現
  const {
    execute: fetchUsers,
    data: users,
    loading,
    error,
  } = useAsync(async () => {
    const res = await fetch("https://reqres.in/api/users/");
    const json = await res.json();
    return json.data;
  });
  
  return (
    // 根據狀態渲染 UI...
  );
}

不過這裡可能有一個疑問:這種型別的封裝我寫一個工具類不就可以了?為啥一定要通過Hooks進行封裝呢?

答案很容易就能想到。應為在Hooks中,你可以管理當前元件的state,從而將更多的邏輯寫在可重用的Hooks中。但是要知道,在普通的工具類中時無法直接修改元件的state的,那麼也就無法在資料改變的時候觸發元件的重新渲染。

  監聽瀏覽器狀態:useScroll

雖然React元件基本上不需要關心太多的瀏覽器API,但是有時候卻是必須的:

1.介面需要根據視窗重新佈局;

2.在頁面滾動時,需要根據滾動位置來決定是否顯示一個”返回頂部“的按鈕。

這都需要用到瀏覽器的api來監聽這些狀態的變化。那麼我們就可以滾動條位置的場景為例,來看看因該如何用Hooks優雅的監聽瀏覽器狀態。

正如Hooks的字面意思時”鉤子“,他帶來的好處就是可以讓React的元件繫結在任何可能的資料來源上。這樣當資料來源發生變化時,元件能夠自動重新整理。把這個好處對應到滾動條這個場景就是:元件需要繫結到滾動條的位置資料上。

雖然這個邏輯在函式元件中能直接實現,但是把這個邏輯實現為一個獨立的Hooks,既可以達到邏輯重用,在語義化也更加清晰。這和上面的useAsync的作用非常類似的。

我們可以直接來看這個Hooks因該如何實現:

import { useState, useEffect } from 'react';

// 獲取橫向,縱向滾動條位置
const getPosition = () => {
  return {
    x: document.body.scrollLeft,
    y: document.body.scrollTop,
  };
};
const useScroll = () => {
  // 定一個 position 這個 state 儲存滾動條位置
  const [position, setPosition] = useState(getPosition());
  useEffect(() => {
    const handler = () => {
      setPosition(getPosition(document));
    };
    // 監聽 scroll 事件,更新滾動條位置
    document.addEventListener("scroll", handler);
    return () => {
      // 元件銷燬時,取消事件監聽
      document.removeEventListener("scroll", handler);
    };
  }, []);
  return position;
};

有了這個 Hook,你就可以非常方便地監聽當前瀏覽器視窗的滾動條位置了。比如下面的程式碼就展示了“返回頂部”這樣一個功能的實現:

import React, { useCallback } from 'react';
import useScroll from './useScroll';

function ScrollTop() {
  const { y } = useScroll();

  const goTop = useCallback(() => {
    document.body.scrollTop = 0;
  }, []);

  const style = {
    position: "fixed",
    right: "10px",
    bottom: "10px",
  };
  // 當滾動條位置縱向超過 300 時,顯示返回頂部按鈕
  if (y > 300) {
    return (
      <button onClick={goTop} style={style}>
        Back to Top
      </button>
    );
  }
  // 否則不 render 任何 UI
  return null;
}
勤學似春起之苗,不見其增,日有所長; 輟學如磨刀之石,不見其損,日所有虧!