useState
const [state, setState] = useState(initialState);
返回一個 state,以及更新 state 的函數。
在初始渲染期間,返回的狀態 (state
) 與傳入的第一個參數 (initialState
) 值相同。
setState
函數用於更新 state。它接收一個新的 state 值並將組件的一次重新渲染加入隊列。
setState(newState);
在後續的重新渲染中,useState 返回的第一個值將始終是更新後最新的 state。
函數式更新
如果新的 state 需要通過使用先前的 state 計算得出,那麼可以將函數傳遞給 setState
。該函數將接收先前的 state,並返回一個更新後的值。下面的計數器組件示例展示了 setState
的兩種用法:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1)
}
function handleClickFn() {
setCount((prevCount) => {
return prevCount + 1
})
}
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>+</button>
</>
);
}
兩種方式的區別
注意上面的代碼,handleClick
和handleClickFn
一個是通過一個新的 state 值更新,一個是通過函數式更新返回新的 state。現在這兩種寫法沒有任何區別,但是如果是異步更新的話,那你就要注意了,他們是有區別的,來看下面例子:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
setCount(count + 1)
}, 3000);
}
function handleClickFn() {
setTimeout(() => {
setCount((prevCount) => {
return prevCount + 1
})
}, 3000);
}
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>+</button>
</>
);
}
當我設置爲異步更新,點擊按鈕延遲到3s之後去調用setCount
函數,當我快速點擊按鈕時,也就是說在3s多次去觸發更新,但是隻有一次生效,因爲 count
的值是沒有變化的。
當使用函數式更新 state 的時候,這種問題就沒有了,因爲它可以獲取之前的 state 值,也就是代碼中的 prevCount
每次都是最新的值。
其實這個特點和類組件中 setState
類似,可以接收一個新的 state 值更新,也可以函數式更新。如果新的 state 需要通過使用先前的 state 計算得出,那麼就要使用函數式更新。
因爲setState更新可能是異步,當你在事件綁定中操作 state 的時候,setState更新就是異步的。
class Counter extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick = () => {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
// 這樣寫只會加1
}
handleClickFn = () => {
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
}
render() {
return (
<>
Count: {this.state.count}
<button onClick={this.handleClick}>+</button>
<button onClick={this.handleClickFn}>+</button>
</>
);
}
}
當你在定時器中操作 state 的時候,而 setState 更新就是同步的。
class Counter extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick = () => {
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
// 這樣寫是正常的,兩次setState最後是加2
}, 3000);
}
handleClickFn = () => {
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
}
render() {
return (
<>
Count: {this.state.count}
<button onClick={this.handleClick}>+</button>
<button onClick={this.handleClickFn}>+</button>
</>
);
}
}
注意這裏的同步和異步指的是 setState 函數。因爲涉及到 state 的狀態合併,react 認爲當你在事件綁定中操作 state 是非常頻繁的,所以爲了節約性能 react 會把多次 setState 進行合併爲一次,最後在一次性的更新 state,而定時器裏面操作 state 是不會把多次合併爲一次更新的。
注意:與 class 組件中的 setState 方法不同,useState 不會自動合併更新對象。
性能優化
React 使用 Object.is 比較算法來比較 state。
在 React 應用中,當某個組件的狀態發生變化時,它會以該組件爲根,重新渲染整個組件子樹。
function Child({ onButtonClick, data }) {
console.log('Child Render')
return (
<button onClick={onButtonClick}>{data.number}</button>
)
}
function App() {
const [number, setNumber] = useState(0)
const [name, setName] = useState('hello') // 表單的值
const addClick = () => setNumber(number + 1)
const data = { number }
return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child onButtonClick={addClick} data={data} />
</div>
)
}
如要避免不必要的子組件的重渲染,使用 React.memo
僅檢查 props 變更。 默認情況下其只會對複雜對象做淺層對比。所有使用 memo 優化後的代碼如下:
function Child({ onButtonClick, data }) {
console.log('Child Render')
return (
<button onClick={onButtonClick}>{data.number}</button>
)
}
Child = memo(Child); // 在這裏優化了
function App() {
const [number, setNumber] = useState(0)
const [name, setName] = useState('hello') // 表單的值
const addClick = () => setNumber(number + 1)
const data = { number }
return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child onButtonClick={addClick} data={data} />
</div>
)
}
你以爲代碼中的Child = memo(Child);
已經優化了嗎,然而並沒有,當你在更改了父組件的狀態,子組件依然會重新渲染,因爲這關係到了React是如何淺層比較的,在子組件中onButtonClick
和 data
都是引用類型,所以他們是始終都不相等的,也就是[]===[]
這樣比較時始終返回false,在基本數據類型比較時memo纔會起作用。
關於如何解決這個問題,我們就要使用兩個新的API,useMemo和useCallback的Hook。下面是經過優化之後的代碼。
function Child({ onButtonClick, data }) {
console.log('Child Render')
return (
<button onClick={onButtonClick}>{data.number}</button>
)
}
Child = memo(Child)
function App() {
const [number, setNumber] = useState(0)
const [name, setName] = useState('hello') // 表單的值
const addClick = useCallback(() => setNumber(number + 1), [number])
const data = useMemo(() => ({ number }), [number])
return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child onButtonClick={addClick} data={data} />
</div>
)
}
export default App;
把“創建”函數和依賴項數組作爲參數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算。如果沒有提供依賴項數組,useMemo 在每次渲染時都會計算新的值。
useCallback返回一個 memoized 回調函數。useCallback(fn, deps)
相當於 useMemo(() => fn, deps)
。
useCallback 和 useMemo 參數相同,第一個參數是函數,第二個參數是依賴項的數組。主要區別是 React.useMemo 將調用 fn 函數並返回其結果,而 React.useCallback 將返回 fn 函數而不調用它。