喔!React19 中的 Hook 可以寫(xiě)在 If 條件判斷中了。Use 實(shí)踐:點(diǎn)擊按鈕更新數(shù)據(jù)
接下來(lái),我們將會(huì)以大量的實(shí)踐案例來(lái)展開(kāi) React 19 新 hook 的運(yùn)用。
本文模擬的實(shí)踐案例為點(diǎn)擊按鈕更新數(shù)據(jù)。這在開(kāi)發(fā)中是一個(gè)非常常見(jiàn)的場(chǎng)景。
案例完成之后的最終演示效果圖如下:
我們直接用 React 19 新的開(kāi)發(fā)方式來(lái)完成這個(gè)需求。
一、基礎(chǔ)實(shí)現(xiàn)
首先創(chuàng)建一個(gè)方法用于請(qǐng)求數(shù)據(jù)。
const getApi = async () => {
const res = await fetch('https://api.chucknorris.io/jokes/random')
return res.json()
}
這里一個(gè)非常關(guān)鍵的地方就在于,當(dāng)我們要更新的數(shù)據(jù)時(shí),我們不再需要設(shè)計(jì)一個(gè) loading 狀態(tài)去記錄數(shù)據(jù)是否正在發(fā)生請(qǐng)求行為,因?yàn)?nbsp;Suspense 幫助我們解決了 Loading 組件的顯示問(wèn)題。
與此同時(shí),use() 又幫助我們解決了數(shù)據(jù)獲取的問(wèn)題。那么問(wèn)題就來(lái)了,這個(gè)就是,好像我們也不需要設(shè)計(jì)一個(gè)狀態(tài)去存儲(chǔ)數(shù)據(jù)。那么應(yīng)該怎么辦呢?
這里有一個(gè)非常巧妙的方式,就是把創(chuàng)建的 promise 作為狀態(tài)值來(lái)觸發(fā)組件的重新執(zhí)行。每次點(diǎn)擊,我們都需要?jiǎng)?chuàng)建新的 promise
代碼如下:
// 記住這個(gè)初始值
const [api, setApi] = useState(null)
這個(gè)時(shí)候,當(dāng)我們點(diǎn)擊事件執(zhí)行時(shí),則只需要執(zhí)行如下代碼去觸發(fā)組件的更新。
function __clickToGetMessage() {
// 每次點(diǎn)擊,都會(huì)創(chuàng)建新的 promise
setApi(getApi())
}
getApi() 執(zhí)行,新的請(qǐng)求會(huì)發(fā)生。他的執(zhí)行結(jié)果,又返回了一個(gè)新的 promise。
因此,點(diǎn)擊之后會(huì)創(chuàng)建的新 promise 值,api 此時(shí)就會(huì)作為狀態(tài)更改觸發(fā)組件的更新。
完整代碼如下:
export default function Index() {
const [api, setApi] = useState(null)
function __clickToGetMessage() {
setApi(getApi())
}
return (
<div>
<div id='tips'>點(diǎn)擊按鈕獲取一條新的數(shù)據(jù)</div>
<button onClick={__clickToGetMessage}>獲取數(shù)據(jù)</button>
<div className="content">
<Suspense fallback={<div>loading...</div>}>
<Item api={api} />
</Suspense>
</div>
</div>
)
}
const Item = (props) => {
if (!props.api) {
return <div>nothing</div>
}
const joke = use(props.api)
return (
<div className='a_value'>{joke.value}</div>
)
}
案例寫(xiě)完之后。我們基本上就能夠?qū)崿F(xiàn)最開(kāi)始截圖中的交互效果了。但是現(xiàn)別急,還沒(méi)有完。我們還需要進(jìn)一步分析一下這個(gè)案例。
二、案例分析
這里我們需要注意觀察兩個(gè)事情。
一個(gè)是觀察當(dāng)前組件更新,更上層的父組件是否發(fā)生了變化。我們可以在 App 組件中執(zhí)行一次打印。
此時(shí)可以發(fā)現(xiàn),當(dāng)我們重新請(qǐng)求時(shí),當(dāng)前組件更新,但是上層組件并不會(huì)重新執(zhí)行。
我們可以出得結(jié)論:更簡(jiǎn)潔的狀態(tài)設(shè)計(jì),有利于命中 React 默認(rèn)的性能優(yōu)化規(guī)則。
具體的規(guī)則請(qǐng)?jiān)?React 知命境合集中查看。
更簡(jiǎn)潔的狀態(tài)設(shè)計(jì),也是 React 19 所倡導(dǎo)的開(kāi)發(fā)思路。
另外一個(gè)事情,是我們要特別特別注意觀察子組件 Item 的實(shí)現(xiàn)。
首先因?yàn)槲覀兂跏蓟瘯r(shí),給 api 賦予的默認(rèn)值是 null。
// 記住這個(gè)初始值
const [api, setApi] = useState(null)
之后,我們就將 api 傳給了子組件 Item。
<Item api={api} />
然后在 Item 組件的內(nèi)部實(shí)現(xiàn)中,因?yàn)槲覀冎苯影?api 傳給了 use,那么此時(shí)直接執(zhí)行肯定會(huì)報(bào)錯(cuò)。
const joke = use(props.api)
要注意的是,我們剛才說(shuō),使用 Suspense 會(huì)捕獲子組件的異常,但是不是捕獲所有異常,它只能識(shí)別 promise 的異常。因此,這里的報(bào)錯(cuò)會(huì)直接影響到整個(gè)頁(yè)面。
所以,為了處理好初始化時(shí)傳入 api 值為 null,我在內(nèi)部實(shí)現(xiàn)代碼邏輯中,使用了 if 判斷該條件,然后執(zhí)行了一次 return。我試圖讓 use(null) 得不到執(zhí)行的時(shí)機(jī)。
const Item = (props) => {
if (!props.api) {
console.log('初始化時(shí),api == null')
return <div>nothing</div>
}
const joke = use(props.api)
return (
<div className='a_value'>{joke.value}</div>
)
}
那么,我的意圖是否能成功呢?
我們?cè)?return 后面插入一個(gè) console.log 來(lái)觀察代碼的執(zhí)行情況,代碼如下:
const Item = (props) => {
if (!props.api) {
console.log('初始化時(shí),api == null')
return <div>nothing</div>
}
console.log('初始化時(shí)這里是否執(zhí)行');
const joke = use(props.api)
return (
<div className='a_value'>{joke.value}</div>
)
}
演示效果如下圖所示:
我們發(fā)現(xiàn),當(dāng)我反復(fù)刷新頁(yè)面,讓初始化流程執(zhí)行時(shí),return 后面的代碼并不會(huì)執(zhí)行。
再然后,我們新增一點(diǎn)內(nèi)容,比如在 return 后面使用一個(gè) useEffect。
const Item = (props) => {
if (!props.api) {
console.log('初始化時(shí),api == null')
return <div>nothing</div>
}
useEffect(() => {
console.log('xxx')
}, [])
console.log('初始化時(shí)這里是否執(zhí)行')
const joke = use(props.api)
return (
<div className='a_value'>{joke.value}</div>
)
}
然后演示再看看。我們發(fā)現(xiàn) effect 也不會(huì)執(zhí)行。然后我們還可以搞點(diǎn)好玩的。
Item 代碼改造如下:
const Item = (props) => {
if (!props.api) {
const [count, setCount] = useState(0)
console.log('初始化時(shí),api == null')
return <div onClick={() => setCount(count + 1)}>nothing, {count}</div>
}
console.log('初始化時(shí)這里是否執(zhí)行')
const joke = use(props.api)
return (
<div className='a_value'>{joke.value}</div>
)
}
注意看,我們?cè)?if 條件判斷中,單獨(dú)創(chuàng)建了一個(gè) useState,并在對(duì)應(yīng)的元素上添加了一個(gè)讓 count 遞增的交互。
這段在之前版本的開(kāi)發(fā)中一定會(huì)觸發(fā)語(yǔ)法錯(cuò)誤提示的代碼。
最終也是能勉強(qiáng)運(yùn)行,但是代碼會(huì)瘋狂報(bào)錯(cuò)。
代碼演示結(jié)果如下:
然后,我繼續(xù)一個(gè)騷操作,我在 if 中條件判斷中,使用 useEffect,代碼如下:
const Item = (props) => {
if (!props.api) {
useEffect(() => {
console.log('useEffect 在 if 中執(zhí)行')
}, [])
return <div>nothing</div>
}
console.log('初始化時(shí)這里是否執(zhí)行')
const joke = use(props.api)
return (
<div className='a_value'>{joke.value}</div>
)
}
也能正常執(zhí)行。觀察一下演示效果:
結(jié)論:
很明顯,react 19 的 hook 在底層發(fā)生了一些優(yōu)化更新,我們可以不用非得把所有的 hook 都放在函數(shù)組件的最前面去執(zhí)行了。
在 React 19 中,我們可以把 hook 放到 return 之后,也可以放到條件判斷中去執(zhí)行。
但是,我們一定要注意的是,并非表示我們可以隨便亂寫(xiě)。當(dāng)條件互斥時(shí),狀態(tài)之間如果存在不合理的耦合關(guān)系,依然不能正常執(zhí)行。我們列舉兩個(gè)案例來(lái)觀察這個(gè)事情。
第一個(gè)案例,我們依然在 if 中執(zhí)行一個(gè) useEffect,但是不同的是,我把在 if 之外的狀態(tài) counter 作為依賴(lài)項(xiàng)傳入。
代碼如下。
const Item = (props) => {
const [counter, setCounter] = useState(0)
if (!props.api) {
useEffect(() => {
console.log('useEffect 在 if 中執(zhí)行')
}, [counter])
return <div>nothing</div>
}
console.log('初始化時(shí)這里是否執(zhí)行')
const joke = use(props.api)
return (
<div className='a_value' onClick={() => setCounter(counter + 1)}>{joke.value}</div>
)
}
此時(shí)一個(gè)很明顯的問(wèn)題就是,if 內(nèi)部在 UI 邏輯上本和外部是互斥的關(guān)系,但是我們?cè)跔顟B(tài)邏輯上卻相互關(guān)聯(lián)。因此這個(gè)之后,代碼執(zhí)行就會(huì)報(bào)錯(cuò),明確的告訴你這種寫(xiě)法不合理。
第二個(gè)案例。我在條件判斷中,定義了一個(gè)狀態(tài) bar,但是我并沒(méi)有在 if 中 return,而是繼續(xù)往后執(zhí)行。代碼如下:
const [counter, setCounter] = useState(0)
if (counter == 0) {
const [bar, setBar] = useState('bar')
console.log('bar', bar)
}
const [foo, setFoo] = useState('foo')
console.log('foo', foo)
return (
<button notallow={() => setCounter(counter + 1)}>counter ++ foo: {foo}</button>
)
這個(gè)現(xiàn)象的解釋就是我們之前在面試時(shí)經(jīng)常會(huì)聊的一個(gè)話(huà)題:為什么不能將 hook 放在條件判斷中去執(zhí)行。
由于在 fiber 中,是通過(guò)有序鏈表的方式來(lái)存儲(chǔ) hook 的值。因此,當(dāng)隨著 counter 遞增,條件判斷中的 hook 不再執(zhí)行,但是它的值已經(jīng)被緩存上了,后續(xù)的執(zhí)行中,foo 就變成了第 1 個(gè) hook,從而導(dǎo)致 foo 獲取到了 bar 的值。
好在 react 19 對(duì)這種情況做出了明確的判斷,當(dāng)你這樣寫(xiě)時(shí),代碼會(huì)明確報(bào)錯(cuò)終止程序的運(yùn)行。所以在開(kāi)發(fā)過(guò)程中我們也不用特別去區(qū)分什么情況下不能用。
三、需求變動(dòng)
現(xiàn)在我們做一點(diǎn)小小的需求變動(dòng)。
在之前的案例實(shí)現(xiàn)中,組件代碼初始化時(shí),并沒(méi)有初始化請(qǐng)求一條數(shù)據(jù)。因此,默認(rèn)渲染結(jié)果是 nothing。
此時(shí),我們?nèi)绻MM件首次渲染時(shí),就一定要請(qǐng)求一次接口,我們的代碼應(yīng)該怎么改呢?
在以前版本的實(shí)現(xiàn)中,接口數(shù)據(jù)的觸發(fā)方式不同,因此我們需要分別處理這兩種觸發(fā)時(shí)機(jī)。
初始化時(shí)的數(shù)據(jù)請(qǐng)求,我們利用 useEffect 來(lái)實(shí)現(xiàn)。
function PreIndex() {
const [data, setData] = useState({value: ''})
const [loading, setLoading] = useState(true)
useEffect(() => {
api().then(res => {
setData(res)
setLoading(false)
})
}, [])
}
按鈕點(diǎn)擊事件觸發(fā)時(shí),我們通過(guò)回調(diào)函數(shù)來(lái)實(shí)現(xiàn)。
function PreIndex() {
const [data, setData] = useState({value: ''})
const [loading, setLoading] = useState(true)
useEffect(() => {
api().then(res => {
setData(res)
setLoading(false)
})
}, [])
function _clickHandler() {
setLoading(true)
api().then(res => {
setData(res)
setLoading(false)
})
}
...
}
然而,在新的開(kāi)發(fā)方式中,我們只需要在上面的案例做一個(gè)非常小的變動(dòng),那就是把 api 的參數(shù)使用 getApi() 去初始化,而不是 null,就可以做到了。
// 只需要改這一點(diǎn)代碼
const [api, setApi] = useState(getApi())
改完之后,演示效果如下:
非常的方便省事。
當(dāng)然這樣寫(xiě)會(huì)造成冗余的接口請(qǐng)求執(zhí)行。因此我們可以稍作調(diào)整就可以了。
這里需要根據(jù)需求調(diào)整,案例只做演示。
const _initApi = getApi()
function Index() {
const [api, setApi] = useState(_initApi)
...
}
OK,今天的案例就介紹到這里,后續(xù)的章節(jié)我們還會(huì)繼續(xù)更多的實(shí)戰(zhàn)案例的分析。