喔!React19 中的 Hook 可以寫在 If 條件判斷中了。Use 實踐:點擊按鈕更新數據

接下來,我們將會以大量的實踐案例來展開 React 19 新 hook 的運用。
本文模擬的實踐案例為點擊按鈕更新數據。這在開發中是一個非常常見的場景。
案例完成之后的最終演示效果圖如下:

我們直接用 React 19 新的開發方式來完成這個需求。
一、基礎實現
首先創建一個方法用于請求數據。
const getApi = async () => {
const res = await fetch('https://api.chucknorris.io/jokes/random')
return res.json()
}這里一個非常關鍵的地方就在于,當我們要更新的數據時,我們不再需要設計一個 loading 狀態去記錄數據是否正在發生請求行為,因為 Suspense 幫助我們解決了 Loading 組件的顯示問題。
與此同時,use() 又幫助我們解決了數據獲取的問題。那么問題就來了,這個就是,好像我們也不需要設計一個狀態去存儲數據。那么應該怎么辦呢?
這里有一個非常巧妙的方式,就是把創建的 promise 作為狀態值來觸發組件的重新執行。每次點擊,我們都需要創建新的 promise
代碼如下:
// 記住這個初始值
const [api, setApi] = useState(null)這個時候,當我們點擊事件執行時,則只需要執行如下代碼去觸發組件的更新。
function __clickToGetMessage() {
// 每次點擊,都會創建新的 promise
setApi(getApi())
}getApi() 執行,新的請求會發生。他的執行結果,又返回了一個新的 promise。
因此,點擊之后會創建的新 promise 值,api 此時就會作為狀態更改觸發組件的更新。
完整代碼如下:
export default function Index() {
const [api, setApi] = useState(null)
function __clickToGetMessage() {
setApi(getApi())
}
return (
<div>
<div id='tips'>點擊按鈕獲取一條新的數據</div>
<button onClick={__clickToGetMessage}>獲取數據</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>
)
}案例寫完之后。我們基本上就能夠實現最開始截圖中的交互效果了。但是現別急,還沒有完。我們還需要進一步分析一下這個案例。
二、案例分析
這里我們需要注意觀察兩個事情。
一個是觀察當前組件更新,更上層的父組件是否發生了變化。我們可以在 App 組件中執行一次打印。
此時可以發現,當我們重新請求時,當前組件更新,但是上層組件并不會重新執行。
我們可以出得結論:更簡潔的狀態設計,有利于命中 React 默認的性能優化規則。
具體的規則請在 React 知命境合集中查看。
更簡潔的狀態設計,也是 React 19 所倡導的開發思路。
另外一個事情,是我們要特別特別注意觀察子組件 Item 的實現。
首先因為我們初始化時,給 api 賦予的默認值是 null。
// 記住這個初始值
const [api, setApi] = useState(null)之后,我們就將 api 傳給了子組件 Item。
<Item api={api} />然后在 Item 組件的內部實現中,因為我們直接把 api 傳給了 use,那么此時直接執行肯定會報錯。
const joke = use(props.api)要注意的是,我們剛才說,使用 Suspense 會捕獲子組件的異常,但是不是捕獲所有異常,它只能識別 promise 的異常。因此,這里的報錯會直接影響到整個頁面。
所以,為了處理好初始化時傳入 api 值為 null,我在內部實現代碼邏輯中,使用了 if 判斷該條件,然后執行了一次 return。我試圖讓 use(null) 得不到執行的時機。
const Item = (props) => {
if (!props.api) {
console.log('初始化時,api == null')
return <div>nothing</div>
}
const joke = use(props.api)
return (
<div className='a_value'>{joke.value}</div>
)
}那么,我的意圖是否能成功呢?
我們在 return 后面插入一個 console.log 來觀察代碼的執行情況,代碼如下:
const Item = (props) => {
if (!props.api) {
console.log('初始化時,api == null')
return <div>nothing</div>
}
console.log('初始化時這里是否執行');
const joke = use(props.api)
return (
<div className='a_value'>{joke.value}</div>
)
}演示效果如下圖所示:

我們發現,當我反復刷新頁面,讓初始化流程執行時,return 后面的代碼并不會執行。
再然后,我們新增一點內容,比如在 return 后面使用一個 useEffect。
const Item = (props) => {
if (!props.api) {
console.log('初始化時,api == null')
return <div>nothing</div>
}
useEffect(() => {
console.log('xxx')
}, [])
console.log('初始化時這里是否執行')
const joke = use(props.api)
return (
<div className='a_value'>{joke.value}</div>
)
}然后演示再看看。我們發現 effect 也不會執行。然后我們還可以搞點好玩的。
Item 代碼改造如下:
const Item = (props) => {
if (!props.api) {
const [count, setCount] = useState(0)
console.log('初始化時,api == null')
return <div onClick={() => setCount(count + 1)}>nothing, {count}</div>
}
console.log('初始化時這里是否執行')
const joke = use(props.api)
return (
<div className='a_value'>{joke.value}</div>
)
}注意看,我們在 if 條件判斷中,單獨創建了一個 useState,并在對應的元素上添加了一個讓 count 遞增的交互。
這段在之前版本的開發中一定會觸發語法錯誤提示的代碼。
最終也是能勉強運行,但是代碼會瘋狂報錯。

代碼演示結果如下:

然后,我繼續一個騷操作,我在 if 中條件判斷中,使用 useEffect,代碼如下:
const Item = (props) => {
if (!props.api) {
useEffect(() => {
console.log('useEffect 在 if 中執行')
}, [])
return <div>nothing</div>
}
console.log('初始化時這里是否執行')
const joke = use(props.api)
return (
<div className='a_value'>{joke.value}</div>
)
}也能正常執行。觀察一下演示效果:

結論:
很明顯,react 19 的 hook 在底層發生了一些優化更新,我們可以不用非得把所有的 hook 都放在函數組件的最前面去執行了。
在 React 19 中,我們可以把 hook 放到 return 之后,也可以放到條件判斷中去執行。
但是,我們一定要注意的是,并非表示我們可以隨便亂寫。當條件互斥時,狀態之間如果存在不合理的耦合關系,依然不能正常執行。我們列舉兩個案例來觀察這個事情。
第一個案例,我們依然在 if 中執行一個 useEffect,但是不同的是,我把在 if 之外的狀態 counter 作為依賴項傳入。
代碼如下。
const Item = (props) => {
const [counter, setCounter] = useState(0)
if (!props.api) {
useEffect(() => {
console.log('useEffect 在 if 中執行')
}, [counter])
return <div>nothing</div>
}
console.log('初始化時這里是否執行')
const joke = use(props.api)
return (
<div className='a_value' onClick={() => setCounter(counter + 1)}>{joke.value}</div>
)
}此時一個很明顯的問題就是,if 內部在 UI 邏輯上本和外部是互斥的關系,但是我們在狀態邏輯上卻相互關聯。因此這個之后,代碼執行就會報錯,明確的告訴你這種寫法不合理。

第二個案例。我在條件判斷中,定義了一個狀態 bar,但是我并沒有在 if 中 return,而是繼續往后執行。代碼如下:
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>
)這個現象的解釋就是我們之前在面試時經常會聊的一個話題:為什么不能將 hook 放在條件判斷中去執行。
由于在 fiber 中,是通過有序鏈表的方式來存儲 hook 的值。因此,當隨著 counter 遞增,條件判斷中的 hook 不再執行,但是它的值已經被緩存上了,后續的執行中,foo 就變成了第 1 個 hook,從而導致 foo 獲取到了 bar 的值。
好在 react 19 對這種情況做出了明確的判斷,當你這樣寫時,代碼會明確報錯終止程序的運行。所以在開發過程中我們也不用特別去區分什么情況下不能用。
三、需求變動
現在我們做一點小小的需求變動。
在之前的案例實現中,組件代碼初始化時,并沒有初始化請求一條數據。因此,默認渲染結果是 nothing。
此時,我們如果希望組件首次渲染時,就一定要請求一次接口,我們的代碼應該怎么改呢?
在以前版本的實現中,接口數據的觸發方式不同,因此我們需要分別處理這兩種觸發時機。
初始化時的數據請求,我們利用 useEffect 來實現。
function PreIndex() {
const [data, setData] = useState({value: ''})
const [loading, setLoading] = useState(true)
useEffect(() => {
api().then(res => {
setData(res)
setLoading(false)
})
}, [])
}按鈕點擊事件觸發時,我們通過回調函數來實現。
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)
})
}
...
}然而,在新的開發方式中,我們只需要在上面的案例做一個非常小的變動,那就是把 api 的參數使用 getApi() 去初始化,而不是 null,就可以做到了。
// 只需要改這一點代碼
const [api, setApi] = useState(getApi())改完之后,演示效果如下:

非常的方便省事。
當然這樣寫會造成冗余的接口請求執行。因此我們可以稍作調整就可以了。
這里需要根據需求調整,案例只做演示。
const _initApi = getApi()
function Index() {
const [api, setApi] = useState(_initApi)
...
}OK,今天的案例就介紹到這里,后續的章節我們還會繼續更多的實戰案例的分析。





































