useEffect 實踐案例:自定義 Hook
我們將在上一章案例的基礎之上學習自定義 hook。
在上一章中,我們巧妙的把大量的 JSX 邏輯處理封裝在了 List 組件中,使得在頁面組件的代碼變得非常簡單。這是針對 UI 層的邏輯處理,那么在數據的處理上,是否也能夠進行一些封裝呢?
// 數據的主要核心邏輯
const str = useRef('')
const [list, setList] = useState<string[]>([])
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
function getList() {
searchApi(str.current).then(res => {
setList(res)
setLoading(false)
setError('')
}).catch(err => {
setLoading(false)
setError(err)
})
}
useEffect(() => {
loading && getList()
}, [loading])
function onSure() {
setLoading(true)
}
答案是肯定的,解決方案就是我們將要在本章中學習的自定義 hook。
一、自定義hook
我們常常會封裝一個函數用于邏輯的復用。自定義 hook 也是這樣的一個在 react 組件內部用于邏輯復用的函數封裝。
和普通函數封裝相比,他唯一的特殊之處就在于我們常常會將 react 內置 hook 封裝在邏輯之中,比如 useState,useEffect 等。除此之外,為了區分與普通的函數封裝,我們必須以 use 開頭為自定義 hook 命名,這樣的 hook 只能在 React 組件中使用。
以上一章中的數據處理邏輯為例,我們來封裝一個自定義 hook,將其命名為 useFetch。
function useFetch() {}
我們先考慮單個場景的封裝,單純只是為了讓組件看上去更簡潔。
我們就可以把所有的數據和處理數據的邏輯封裝起來。
import {useEffect, useState, useRef} from 'react'
import { searchApi } from './api'
export default function useFetch() {
const str = useRef('')
const [list, setList] = useState<string[]>([])
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
function getList() {
searchApi(str.current).then(res => {
setList(res)
setLoading(false)
setError('')
}).catch(err => {
setLoading(false)
setError(err)
})
}
useEffect(() => {
loading && getList()
}, [loading])
return { str, list, error, loading, setLoading }
}
封裝過程非常簡單,就是把之前那一堆邏輯全部遷移過來,最后返回應用組件里需要的數據和方法即可。
return { str, list, error, loading, setLoading }
OK,此時我們來觀察一下組件里的代碼。
export default function DemoOneNormal() {
const {loading, setLoading, str, list, error} = useFetch()
return (
<Block className={s.container} title={td.title} desc={td.desc}>
<div className={r.flex}>
<input
className={s.input}
placeholder="請輸入您要搜索的內容"
onChange={(e) => str.current = e.target.value}
/>
<Button
className={s.button}
onClick={() => setLoading(true)}
>
搜索
</Button>
</div>
<List
list={list}
loading={loading}
error={error}
renderItem={(item) => (
<div key={item} className={s.item}>{item}</div>
)}
/>
</Block>
)
}
邏輯簡潔了許多。變成了簡單的同步代碼:通過一個方法獲取數據,并將數據渲染到 UI 組件。
Block 組件是單獨封裝的布局組件,希望不要因此造成任何理解上的困難。
一個組件變成了數據與UI的結合。我們分別將復雜的數據處理邏輯封裝在 hook 里,將復雜的UI交互邏輯封裝在基礎 UI 組件里,在使用時,利用他們的封裝結果進行組合,能夠簡單,高效的組合出復雜的頁面,這也是我們在實踐中最大的追求
這里有些人可能會有一些疑問,我只是把一些邏輯放在了另外的地方,代碼量最終不僅沒有減少,反而還變多了,這樣做的好處真的有那么大嗎?當然,因為我們封裝的 useFetch 和 List 組件,他們承載了大多數的復雜邏輯,并且只會在最開始的時候編寫一次,在以后的使用中,就直接引入使用就行了,這極大的簡化了后續的開發工作量,對工作效率的提高非常顯著
二、進一步思考
此時的封裝雖然足夠簡潔。但是沒有考慮復用。因此還需要進一步思考改進。
我們來分析一下場景:每一個需要信息展示的頁面,基本邏輯都是在初始化時,請求接口,獲得數據,然后展示信息。我們可以把不同情況的接口請求抽象成為一個接口,然后基于這個場景來思考不同頁面的請求的共性與差異。
每個頁面都要處理信息展示、異常等邏輯,差異的地方就在于獲取數據的 api 函數不一樣,他返回的數據內容,數據類型也不一樣。
不一樣的東西作為參數傳入,那我們只需要將 api 函數作為參數傳入即可。
const info = useFetch(searchApi)
不過我們此時還需要考慮的是,為了確保自定義 hook 的返回類型具備完整準確的類型推導,我們還需要約定傳入 api 的參數類型與返回類型。
因此,在定義 useFetch 時,我們先用 ts 約定 api 的具體類型,因為參數類型和返回值類型在封裝時都不確定,只能在具體的實參傳入之后才能明確,因此使用兩個泛型來分別表示參數類型和返回值類型。
type API<T, P>
= (param?: P) => Promise<T>
正常代碼不會這樣換行,之所以這樣只是為了在移動端能夠更多的展示代碼信息而不用滾動查看。
然后在定義 useFetch 時傳入這兩個泛型即可,完整代碼如下:
import { useEffect, useState, useRef } from 'react'
type API<T, P> = (param?: P) => Promise<T>
export default function useFetch<T, P>(api: API<T, P>) {
const param = useRef<P>()
const [list, setList] = useState<T>()
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
function getList() {
api(param.current).then(res => {
setList(res)
setLoading(false)
setError('')
}).catch(err => {
setLoading(false)
setError(err)
})
}
useEffect(() => {
loading && getList()
}, [loading])
return {
param,
setParam: (p: P) => param.current = p,
list,
error,
loading,
setLoading
}
}
因為在使用時,傳入的 api 函數已經具備了完善的類型,因此我們這種寫法可以借助 ts 內部的自動推導而簡化使用時在 ts 上的繁瑣。
const {
loading,
setLoading,
setParam,
list,
error
} = useFetch(searchApi)
雖然在使用層面沒有任何 ts 的痕跡,但是返回值的類型已經非常明確。
由于在封裝過程中我們沒有處理默認值的情況,因此返回類型可能為 undefined,這在實踐中一定要引起重視。你可以根據實際情況往 useFetch 傳入默認值,也可以在使用層面初始化默認值
const {
loading,
setLoading,
setParam,
list = [],
error
} = useFetch(searchApi)
這樣,一個通用,高效,且具備準確類型提示的 hook 就被我們封裝好了。
在實踐過程中,由于不同的團隊有不同的需求,你還需要根據自己的需求和項目實際情況做相應的細節調整,切記不要完整套用。
三、取舍
由于面試的影響,讓不少前端同行錯誤的把性能當成了實踐中最重要的標準。但其實工作中性能并不是最高的優先級。我們往往會在可接受的范圍之內,犧牲性能換取其他的便利。
例如,多一層函數封裝,其實也就意味著執行壓力多那么一點點。但是他可能換來的是開發效率的極大提高。
因此,在我們的課程案例決策當中,提供的方案并不會把性能當做第一準則,代碼的可讀性、可維護性、開發效率的優先級都會比性能更高。只要我們在寫代碼的過程中,非常明確的知道這種方式我們舍棄了什么,得到了什么,你權衡之后,愿意做出這樣的取舍,那么這樣的方式就是可以使用的。
當然,性能依然非常重要,如果你的頁面出現了卡頓,我們就應該思考一下,是不是對性能的犧牲有點過了頭。