自定義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; }勤學似春起之苗,不見其增,日有所長; 輟學如磨刀之石,不見其損,日所有虧!