作者:Dmitri Pavlutin
譯者:前端小智
來源:dmitripavlutin.com
你知道的越多,你不知道的越多
點贊再看,養成習慣
本文 GitHub:github.com/qq449245884… 上已經收錄,更多往期高贊文章的分類,也整理了很多我的文件,和教程資料。歡迎Star和完善,大家面試可以參照考點複習,希望我們一起有點東西。
狀態是隱藏在元件中的資訊,元件可以在父元件不知道的情況下修改其狀態。我更偏愛函式元件,因為它們足夠簡單,要使函式元件具有狀態管理,可以useState()
Hook。
本文會逐步講解如何使用useState()
Hook。此外,還會介紹一些常見useState()
坑。
1.使用 useState()
進行狀態管理
無狀態的函式元件沒有狀態,如下所示(部分程式碼):
import React from 'react';
function Bulbs() {
return <div className="bulb-off" />;
}
複製程式碼
可以找 codesandbox 嘗試一下。
執行效果:
這時,要如何新增一個按鈕來開啟/關閉燈泡呢? 為此,我們們需要具有狀態的函式元件,也就是有狀態函式元件。
useState()
是實現燈泡開關狀態的 Hoook,將狀態新增到函式元件需要4
個步驟:啟用狀態、初始化、讀取和更新。
1.1 啟用狀態
要將<Bulbs>
轉換為有狀態元件,需要告訴 React:從'react'
包中匯入useState
鉤子,然後在元件函式的頂部呼叫useState()
。
大致如下所示:
import React, { useState } from 'react';
function Bulbs() {
... = useState(...);
return <div className="bulb-off" />;
}
複製程式碼
在Bulbs
函式的第一行呼叫useState()
(暫時不要考Hook的引數和返回值)。 重要的是,在元件內部呼叫 Hook 會使該函式成為有狀態的函式元件。
啟用狀態後,下一步是初始化它。
1.2初始化狀態
始時,燈泡關閉,對應到狀態應使用false
初始化 Hook:
import React, { useState } from 'react';
function Bulbs() {
... = useState(false);
return <div className="bulb-off" />;
}
複製程式碼
useState(false)
用false
初始化狀態。
啟用和初始化狀態之後,如何讀取它?來看看useState(false)
返回什麼。
1.3 讀取狀態
當 hook useState(initialState)
被呼叫時,它返回一個陣列,該陣列的第一項是狀態值
const stateArray = useState(false);
stateArray[0]; // => 狀態值
複製程式碼
我們們讀取元件的狀態
function Bulbs() {
const stateArray = useState(false);
return <div className={stateArray[0] ? 'bulb-on' : 'bulb-off'} />;
}
複製程式碼
<Bulbs>
元件狀態初始化為false
,可以開啟 codesandbox 看看效果。
useState(false)
返回一個陣列,第一項包含狀態值,該值當前為false
(因為狀態已用false
初始化)。
我們們可以使用陣列解構來將狀態值提取到變數on
上:
import React, { useState } from 'react';
function Bulbs() {
const [on] = useState(false);
return <div className={on ? 'bulb-on' : 'bulb-off'} />;
}
複製程式碼
on
狀態變數儲存狀態值。
狀態已經啟用並初始化,現在可以讀取它了。但是如何更新呢?再來看看useState(initialState)
返回什麼。
####1.4 更新狀態
用值更新狀態
我們們已經知道,useState(initialState)
返回一個陣列,其中第一項是狀態值,第二項是一個更新狀態的函式。
const [state, setState] = useState(initialState);
// 將狀態更改為 'newState' 並觸發重新渲染
setState(newState);
// 重新渲染`state`後的值為`newState`
複製程式碼
要更新元件的狀態,請使用新狀態呼叫更新器函式setState(newState)
。元件重新渲染後,狀態接收新值newState
。
當點選開燈
按鈕時將燈泡開關狀態更新為true
,點選關燈
時更新為 false
。
import React, { useState } from 'react';
function Bulbs() {
const [on, setOn] = useState(false);
const lightOn = () => setOn(true);
const lightOff = () => setOn(false);
return (
<>
<div className={on ? 'bulb-on' : 'bulb-off'} />
<button onClick={lightOn}>開燈</button>
<button onClick={lightOff}>關燈</button>
</>
);
}
複製程式碼
開啟 codesandbox 自行嘗試一下。
單擊開燈按鈕時,lightOn()
函式將on
更新為true
: setOn(true)
。單擊關燈時也會發生相同的情況,只是狀態更新為false
。
狀態一旦改變,React 就會重新渲染元件,on
變數獲取新的狀態值。
狀態更新作為對提供一些新資訊的事件的響應。這些事件包括按鈕單擊、HTTP 請求完成等,確保在事件回撥或其他 Hook 回撥中呼叫狀態更新函式。
使用回撥更新狀態
當使用前一個狀態計算新狀態時,可以使用回撥更新該狀態:
const [state, setState] = useState(initialState);
...
setState(prevState => nextState);
...
複製程式碼
下面是一些事例:
// Toggle a boolean
const [toggled, setToggled] = useState(false);
setToggled(toggled => !toggled);
// Increase a counter
const [count, setCount] = useState(0);
setCount(count => count + 1);
// Add an item to array
const [items, setItems] = useState([]);
setItems(items => [...items, 'New Item']);
複製程式碼
接著,通過這種方式重新實現上面電燈的示例:
import React, { useState } from 'react';
function Bulbs() {
const [on, setOn] = useState(false);
const lightSwitch = () => setOn(on => !on);
return (
<>
<div className={on ? 'bulb-on' : 'bulb-off'} />
<button onClick={lightSwitch}>開燈/關燈</button>
</>
);
}
複製程式碼
開啟 codesandbox 自行嘗試一下。
setOn(on => !on)
使用函式更新狀態。
1.5 小結一波
-
呼叫
useState()
Hook 來啟用函式元件中的狀態。 -
useState(initialValue)
的第一個引數initialValue
是狀態的初始值。 -
[state, setState] = useState(initialValue)
返回一個包含2
個元素的陣列:狀態值和狀態更新函式。 -
使用新值呼叫狀態更新器函式
setState(newState)
更新狀態。或者,可以使用一個回撥setState(prev => next)
來呼叫狀態更新器,該回撥將返回基於先前狀態的新狀態。 -
呼叫狀態更新器後,React 確保重新渲染元件,以使新狀態變為當前狀態。
2. 多種狀態
通過多次呼叫useState()
,一個函式元件可以擁有多個狀態。
function MyComponent() {
const [state1, setState1] = useState(initial1);
const [state2, setState2] = useState(initial2);
const [state3, setState3] = useState(initial3);
// ...
}
複製程式碼
需要注意的,要確保對useState()
的多次呼叫在渲染之間始終保持相同的順序(後面會講)。
我們新增一個按鈕新增燈泡
,並新增一個新狀態來儲存燈泡數量,單擊該按鈕時,將新增一個新燈泡。
新的狀態count
包含燈泡的數量,初始值為1
:
import React, { useState } from 'react';
function Bulbs() {
const [on, setOn] = useState(false);
const [count, setCount] = useState(1);
const lightSwitch = () => setOn(on => !on);
const addBulbs = () => setCount(count => count + 1);
const bulb = <div className={on ? 'bulb-on' : 'bulb-off'} />;
const bulbs = Array(count).fill(bulb);
return (
<>
<div className="bulbs">{bulbs}</div>
<button onClick={lightSwitch}>開/關</button>
<button onClick={addBulbs}>新增燈泡</button>
</>
);
}
複製程式碼
開啟演示,然後單擊新增燈泡按鈕:燈泡數量增加,單擊開/關按鈕可開啟/關閉燈泡。
- [on, setOn] = useState(false) 管理開/關狀態
[count, setCount] = useState(1)
管理燈泡數量。
多個狀態可以在一個元件中正確工作。
3.狀態的延遲初始化
每當 React 重新渲染元件時,都會執行useState(initialState)
。 如果初始狀態是原始值(數字,布林值等),則不會有效能問題。
當初始狀態需要昂貴的效能方面的操作時,可以通過為useState(computeInitialState)
提供一個函式來使用狀態的延遲初始化,如下所示:
function MyComponent({ bigJsonData }) {
const [value, setValue] = useState(function getInitialState() {
const object = JSON.parse(bigJsonData); // expensive operation
return object.initialValue;
});
// ...
}
複製程式碼
getInitialState()
僅在初始渲染時執行一次,以獲得初始狀態。在以後的元件渲染中,不會再呼叫getInitialState()
,從而跳過昂貴的操作。
4. useState() 中的坑
現在我們們基本已經初步掌握瞭如何使用useState()
,儘管如此,我們們必須注意在使用useState()
時可能遇到的常見問題。
4.1 在哪裡呼叫 useState()
在使用useState()
Hook 時,必須遵循 Hook 的規則
-
僅頂層呼叫 Hook :不能在迴圈,條件,巢狀函式等中呼叫
useState()
。在多個useState()
呼叫中,渲染之間的呼叫順序必須相同。 -
僅從React 函式呼叫 Hook:必須僅在函式元件或自定義鉤子內部呼叫
useState()
。
來看看useState()
的正確用法和錯誤用法的例子。
有效呼叫useState()
useState()
在函式元件的頂層被正確呼叫
function Bulbs() {
// Good
const [on, setOn] = useState(false);
// ...
}
複製程式碼
以相同的順序正確地呼叫多個useState()
呼叫:
function Bulbs() {
// Good
const [on, setOn] = useState(false);
const [count, setCount] = useState(1);
// ...
複製程式碼
useState()
在自定義鉤子的頂層被正確呼叫
function toggleHook(initial) {
// Good
const [on, setOn] = useState(initial);
return [on, () => setOn(!on)];
}
function Bulbs() {
const [on, toggle] = toggleHook(false);
// ...
}
複製程式碼
useState()
的無效呼叫
在條件中呼叫useState()
是不正確的:
function Switch({ isSwitchEnabled }) {
if (isSwitchEnabled) {
// Bad
const [on, setOn] = useState(false);
}
// ...
}
複製程式碼
在巢狀函式中呼叫useState()
也是不對的
function Switch() {
let on = false;
let setOn = () => {};
function enableSwitch() {
// Bad
[on, setOn] = useState(false);
}
return (
<button onClick={enableSwitch}>
Enable light switch state
</button>
);
}
複製程式碼
4.2 過時狀態
閉包是一個從外部作用域捕獲變數的函式。
閉包(例如事件處理程式,回撥)可能會從函式元件作用域中捕獲狀態變數。 由於狀態變數在渲染之間變化,因此閉包應捕獲具有最新狀態值的變數。否則,如果閉包捕獲了過時的狀態值,則可能會遇到過時的狀態問題。
來看看一個過時的狀態是如何表現出來的。元件<DelayedCount>
延遲3
秒計數按鈕點選的次數。
function DelayedCount() {
const [count, setCount] = useState(0);
const handleClickAsync = () => {
setTimeout(function delay() {
setCount(count + 1);
}, 3000);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>Increase async</button>
</div>
);
}
複製程式碼
開啟演示,快速多次點選按鈕。count
變數不能正確記錄實際點選次數,有些點選被吃掉。
delay()
是一個過時的閉包,它從初始渲染(使用0
初始化時)中捕獲了過時的count
變數。
為了解決這個問題,使用函式方法來更新count
狀態:
function DelayedCount() {
const [count, setCount] = useState(0);
const handleClickAsync = () => {
setTimeout(function delay() {
setCount(count => count + 1);
}, 3000);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>Increase async</button>
</div>
);
}
複製程式碼
現在etCount(count => count + 1)
在delay()
中正確更新計數狀態。React 確保將最新狀態值作為引數提供給更新狀態函式,過時閉包的問題解決了。
開啟演示,快速單擊按鈕。 延遲過去後,count
能正確表示點選次數。
4.3 複雜狀態管理
useState()
用於管理簡單狀態。對於複雜的狀態管理,可以使用useReducer()
hook。它為需要多個狀態操作的狀態提供了更好的支援。
假設需要編寫一個最喜歡的電影列表。使用者可以新增電影,也可以刪除已有的電影,實現方式大致如下:
import React, { useState } from 'react';
function FavoriteMovies() {
const [movies, setMovies] = useState([{ name: 'Heat' }]);
const add = movie => setMovies([...movies, movie]);
const remove = index => {
setMovies([
...movies.slice(0, index),
...movies.slice(index + 1)
]);
}
return (
// Use add(movie) and remove(index)...
);
}
複製程式碼
嘗試演示:新增和刪除自己喜歡的電影。
狀態列表需要幾個操作:新增和刪除電影,狀態管理細節使元件混亂。
更好的解決方案是將複雜的狀態管理提取到reducer
中:
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'add':
return [...state, action.item];
case 'remove':
return [
...state.slice(0, action.index),
...state.slice(action.index + 1)
];
default:
throw new Error();
}
}
function FavoriteMovies() {
const [state, dispatch] = useReducer(reducer, [{ name: 'Heat' }]);
return (
// Use dispatch({ type: 'add', item: movie })
// and dispatch({ type: 'remove', index })...
);
}
複製程式碼
reducer
管理電影的狀態,有兩種操作型別:
-
"add"
將新電影插入列表 -
"remove"
從列表中按索引刪除電影
嘗試演示並注意元件功能沒有改變。但是這個版本的<FavoriteMovies>
更容易理解,因為狀態管理已經被提取到reducer
中。
還有一個好處:可以將reducer
提取到一個單獨的模組中,並在其他元件中重用它。另外,即使沒有元件,也可以對reducer
進行單元測試。
這就是關注點分離的威力:元件渲染UI並響應事件,而reducer
執行狀態操作。
4.4 狀態 vs 引用
考慮這樣一個場景:我們們想要計算元件渲染的次數。
一種簡單的實現方法是初始化countRender
狀態,並在每次渲染時更新它(使用useEffect()
hook)
import React, { useState, useEffect } from 'react';
function CountMyRenders() {
const [countRender, setCountRender] = useState(0);
useEffect(function afterRender() {
setCountRender(countRender => countRender + 1);
});
return (
<div>I've rendered {countRender} times</div>
);
}
複製程式碼
useEffect()
在每次渲染後呼叫afterRender()
回撥。但是一旦countRender
狀態更新,元件就會重新渲染。這將觸發另一個狀態更新和另一個重新渲染,依此類推。
可變引用useRef()
儲存可變資料,這些資料在更改時不會觸發重新渲染,使用可變的引用改造一下<CountMyRenders>
:
import React, { useRef, useEffect } from 'react';
function CountMyRenders() {
const countRenderRef = useRef(1);
useEffect(function afterRender() {
countRenderRef.current++;
});
return (
<div>I've rendered {countRenderRef.current} times</div>
);
}
複製程式碼
開啟演示並單擊幾次按鈕來觸發重新渲染。
每次渲染元件時,countRenderRef
可變引用的值都會使countRenderRef.current ++
遞增。 重要的是,更改不會觸發元件重新渲染。
5. 總結
要使函式元件有狀態,請在元件的函式體中呼叫useState()
。
useState(initialState)
的第一個引數是初始狀態。返回的陣列有兩項:當前狀態和狀態更新函式。
const [state, setState] = useState(initialState);
複製程式碼
使用 setState(newState)
來更新狀態值。 另外,如果需要根據先前的狀態更新狀態,可以使用回撥函式setState(prevState => newState)
。
在單個元件中可以有多個狀態:呼叫多次useState()
。
當初始狀態開銷很大時,延遲初始化很方便。使用計算初始狀態的回撥呼叫useState(computeInitialState)
,並且此回撥僅在初始渲染時執行一次。
必須確保使用useState()
遵循 Hook 規則。
當閉包捕獲過時的狀態變數時,就會出現過時狀態的問題。可以通過使用一個回撥來更新狀態來解決這個問題,這個回撥會根據先前的狀態來計算新的狀態。
最後,您將使用useState()
來管理一個簡單的狀態。為了處理更復雜的狀態,一個更好的的選擇是使用useReducer()
hook。
原文:dmitripavlutin.com/react-usest…
程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
交流(歡迎加入群,群工作日都會發紅包,互動討論技術)
乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。
因為篇幅的限制,今天的分享只到這裡。如果大家想了解更多的內容的話,可以去掃一掃每篇文章最下面的二維碼,然後關注我們們的微信公眾號,瞭解更多的資訊和有價值的內容。