useEffect 實(shí)踐案例:自定義 Hook
我們將在上一章案例的基礎(chǔ)之上學(xué)習(xí)自定義 hook。
在上一章中,我們巧妙的把大量的 JSX 邏輯處理封裝在了 List 組件中,使得在頁(yè)面組件的代碼變得非常簡(jiǎn)單。這是針對(duì) UI 層的邏輯處理,那么在數(shù)據(jù)的處理上,是否也能夠進(jìn)行一些封裝呢?
// 數(shù)據(jù)的主要核心邏輯
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)
}
答案是肯定的,解決方案就是我們將要在本章中學(xué)習(xí)的自定義 hook。
一、自定義hook
我們常常會(huì)封裝一個(gè)函數(shù)用于邏輯的復(fù)用。自定義 hook 也是這樣的一個(gè)在 react 組件內(nèi)部用于邏輯復(fù)用的函數(shù)封裝。
和普通函數(shù)封裝相比,他唯一的特殊之處就在于我們常常會(huì)將 react 內(nèi)置 hook 封裝在邏輯之中,比如 useState,useEffect 等。除此之外,為了區(qū)分與普通的函數(shù)封裝,我們必須以 use 開(kāi)頭為自定義 hook 命名,這樣的 hook 只能在 React 組件中使用。
以上一章中的數(shù)據(jù)處理邏輯為例,我們來(lái)封裝一個(gè)自定義 hook,將其命名為 useFetch。
function useFetch() {}
我們先考慮單個(gè)場(chǎng)景的封裝,單純只是為了讓組件看上去更簡(jiǎn)潔。
我們就可以把所有的數(shù)據(jù)和處理數(shù)據(jù)的邏輯封裝起來(lái)。
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 }
}
封裝過(guò)程非常簡(jiǎn)單,就是把之前那一堆邏輯全部遷移過(guò)來(lái),最后返回應(yīng)用組件里需要的數(shù)據(jù)和方法即可。
return { str, list, error, loading, setLoading }
OK,此時(shí)我們來(lái)觀察一下組件里的代碼。
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="請(qǐng)輸入您要搜索的內(nèi)容"
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>
)
}
邏輯簡(jiǎn)潔了許多。變成了簡(jiǎn)單的同步代碼:通過(guò)一個(gè)方法獲取數(shù)據(jù),并將數(shù)據(jù)渲染到 UI 組件。
Block 組件是單獨(dú)封裝的布局組件,希望不要因此造成任何理解上的困難。
一個(gè)組件變成了數(shù)據(jù)與UI的結(jié)合。我們分別將復(fù)雜的數(shù)據(jù)處理邏輯封裝在 hook 里,將復(fù)雜的UI交互邏輯封裝在基礎(chǔ) UI 組件里,在使用時(shí),利用他們的封裝結(jié)果進(jìn)行組合,能夠簡(jiǎn)單,高效的組合出復(fù)雜的頁(yè)面,這也是我們?cè)趯?shí)踐中最大的追求
這里有些人可能會(huì)有一些疑問(wèn),我只是把一些邏輯放在了另外的地方,代碼量最終不僅沒(méi)有減少,反而還變多了,這樣做的好處真的有那么大嗎?當(dāng)然,因?yàn)槲覀兎庋b的 useFetch 和 List 組件,他們承載了大多數(shù)的復(fù)雜邏輯,并且只會(huì)在最開(kāi)始的時(shí)候編寫一次,在以后的使用中,就直接引入使用就行了,這極大的簡(jiǎn)化了后續(xù)的開(kāi)發(fā)工作量,對(duì)工作效率的提高非常顯著
二、進(jìn)一步思考
此時(shí)的封裝雖然足夠簡(jiǎn)潔。但是沒(méi)有考慮復(fù)用。因此還需要進(jìn)一步思考改進(jìn)。
我們來(lái)分析一下場(chǎng)景:每一個(gè)需要信息展示的頁(yè)面,基本邏輯都是在初始化時(shí),請(qǐng)求接口,獲得數(shù)據(jù),然后展示信息。我們可以把不同情況的接口請(qǐng)求抽象成為一個(gè)接口,然后基于這個(gè)場(chǎng)景來(lái)思考不同頁(yè)面的請(qǐng)求的共性與差異。
每個(gè)頁(yè)面都要處理信息展示、異常等邏輯,差異的地方就在于獲取數(shù)據(jù)的 api 函數(shù)不一樣,他返回的數(shù)據(jù)內(nèi)容,數(shù)據(jù)類型也不一樣。
不一樣的東西作為參數(shù)傳入,那我們只需要將 api 函數(shù)作為參數(shù)傳入即可。
const info = useFetch(searchApi)
不過(guò)我們此時(shí)還需要考慮的是,為了確保自定義 hook 的返回類型具備完整準(zhǔn)確的類型推導(dǎo),我們還需要約定傳入 api 的參數(shù)類型與返回類型。
因此,在定義 useFetch 時(shí),我們先用 ts 約定 api 的具體類型,因?yàn)閰?shù)類型和返回值類型在封裝時(shí)都不確定,只能在具體的實(shí)參傳入之后才能明確,因此使用兩個(gè)泛型來(lái)分別表示參數(shù)類型和返回值類型。
type API<T, P>
= (param?: P) => Promise<T>
正常代碼不會(huì)這樣換行,之所以這樣只是為了在移動(dòng)端能夠更多的展示代碼信息而不用滾動(dòng)查看。
然后在定義 useFetch 時(shí)傳入這兩個(gè)泛型即可,完整代碼如下:
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
}
}
因?yàn)樵谑褂脮r(shí),傳入的 api 函數(shù)已經(jīng)具備了完善的類型,因此我們這種寫法可以借助 ts 內(nèi)部的自動(dòng)推導(dǎo)而簡(jiǎn)化使用時(shí)在 ts 上的繁瑣。
const {
loading,
setLoading,
setParam,
list,
error
} = useFetch(searchApi)
雖然在使用層面沒(méi)有任何 ts 的痕跡,但是返回值的類型已經(jīng)非常明確。
由于在封裝過(guò)程中我們沒(méi)有處理默認(rèn)值的情況,因此返回類型可能為 undefined,這在實(shí)踐中一定要引起重視。你可以根據(jù)實(shí)際情況往 useFetch 傳入默認(rèn)值,也可以在使用層面初始化默認(rèn)值
const {
loading,
setLoading,
setParam,
list = [],
error
} = useFetch(searchApi)
這樣,一個(gè)通用,高效,且具備準(zhǔn)確類型提示的 hook 就被我們封裝好了。
在實(shí)踐過(guò)程中,由于不同的團(tuán)隊(duì)有不同的需求,你還需要根據(jù)自己的需求和項(xiàng)目實(shí)際情況做相應(yīng)的細(xì)節(jié)調(diào)整,切記不要完整套用。
三、取舍
由于面試的影響,讓不少前端同行錯(cuò)誤的把性能當(dāng)成了實(shí)踐中最重要的標(biāo)準(zhǔn)。但其實(shí)工作中性能并不是最高的優(yōu)先級(jí)。我們往往會(huì)在可接受的范圍之內(nèi),犧牲性能換取其他的便利。
例如,多一層函數(shù)封裝,其實(shí)也就意味著執(zhí)行壓力多那么一點(diǎn)點(diǎn)。但是他可能換來(lái)的是開(kāi)發(fā)效率的極大提高。
因此,在我們的課程案例決策當(dāng)中,提供的方案并不會(huì)把性能當(dāng)做第一準(zhǔn)則,代碼的可讀性、可維護(hù)性、開(kāi)發(fā)效率的優(yōu)先級(jí)都會(huì)比性能更高。只要我們?cè)趯懘a的過(guò)程中,非常明確的知道這種方式我們舍棄了什么,得到了什么,你權(quán)衡之后,愿意做出這樣的取舍,那么這樣的方式就是可以使用的。
當(dāng)然,性能依然非常重要,如果你的頁(yè)面出現(xiàn)了卡頓,我們就應(yīng)該思考一下,是不是對(duì)性能的犧牲有點(diǎn)過(guò)了頭。