
useState
使用狀態(tài)
const [n, setN] = React.useState(0)
const [user, setUser] = React.useState({name: 'Jack', age: 18})
- 注意事項1: 不可局部更新
如果state是一個對象,能否部分setState?
答案是不行,因為setState不會幫我們合并屬性
那么useReducer會合并屬性嗎?也不會!
因為React認為這應該是你自己要做的事情
function App(){
const [user, setUser] = React.useState({name: 'Jack', age: 18})
const onClick = () =>{
//setUser不可以局部更新,如果只改變其中一個,那么整個數(shù)據(jù)都會被覆蓋
// setUser({
// name: 'Frank'
// })
setUser({
...user, //拷貝之前的所有屬性
name: 'Frank' //這里的name覆蓋之前的name
})
}
return (
<div className='App'>
<h1>{user.name}</h1>
<h2>{user.age}</h2>
<button onClick={onClick}>Click</button>
</div>
)
}
- 注意事項2: 地址要變
setState(obj) 如果obj內(nèi)存地址不變,那么React就認為數(shù)據(jù)沒有變化,不會更新視圖,了解內(nèi)存地址與 桟堆概念
- useState接受函數(shù)
const [state, setState] = useState(() => {return initialState})
該函數(shù)返回初始state,且只執(zhí)行一次
- setState接受函數(shù)
setxxxx(i => i + 1)
如果你能接受這種形式,應該優(yōu)先使用這種形式
修改之后的狀態(tài) 會在函數(shù)組件 再次更新完之后 拿到最新的值,如果想立刻拿到最新的值,使用useRef
useRef
useRef這個hooks函數(shù),除了傳統(tǒng)的用法之外,它還可以“跨渲染周期”保存數(shù)據(jù)。
import React, { useState, useEffect, useMemo, useRef } from 'react';
export default function App(props){
const [count, setCount] = useState(0);
const doubleCount = useMemo(() => {
return 2 * count;
}, [count]);
const couterRef = useRef();
useEffect(() => {
document.title = `The value is ${count}`;
console.log(couterRef.current);
}, [count]);
return (
<>
<button ref={couterRef} onClick={() => {setCount(count + 1)}}>Count: {count}, double: {doubleCount}</button>
</>
);
}
代碼中用useRef創(chuàng)建了couterRef對象,并將其賦給了button的ref屬性。這樣,通過訪問couterRef.current就可以訪問到button對應的DOM對象。
然后再來看看它保存數(shù)據(jù)的用法。
在一個組件中有什么東西可以跨渲染周期,也就是在組件被多次渲染之后依舊不變的屬性?第一個想到的應該是state。沒錯,一個組件的state可以在多次渲染之后依舊不變。但是,state的問題在于一旦修改了它就會造成組件的重新渲染。
那么這個時候就可以使用useRef來跨越渲染周期存儲數(shù)據(jù),而且對它修改也不會引起組件渲染。
import React, { useState, useEffect, useMemo, useRef } from 'react';
export default function App(props){
const [count, setCount] = useState(0);
const doubleCount = useMemo(() => {
return 2 * count;
}, [count]);
const timerID = useRef();
useEffect(() => {
timerID.current = setInterval(()=>{
setCount(count => count + 1);
}, 1000);
}, []);
useEffect(()=>{
if(count > 10){
clearInterval(timerID.current);
}
});
return (
<>
<button ref={couterRef} onClick={() => {setCount(count + 1)}}>Count: {count}, double: {doubleCount}</button>
</>
);
}
在上面的例子中,我用ref對象的current屬性來存儲定時器的ID,這樣便可以在多次渲染之后依舊保存定時器ID,從而能正常清除定時器。
useReducer():action 鉤子
React 本身不提供狀態(tài)管理功能,通常需要使用外部庫。這方面最常用的庫是 Redux。
Redux 的核心概念是,組件發(fā)出 action 與狀態(tài)管理器通信。狀態(tài)管理器收到 action 以后,使用 Reducer 函數(shù)算出新的狀態(tài),Reducer 函數(shù)的形式是(state, action) => newState。
useReducers()鉤子用來引入 Reducer 功能。
const [state, dispatch] = useReducer(reducer, initialState);
上面是useReducer()的基本用法,它接受 Reducer 函數(shù)和狀態(tài)的初始值作為參數(shù),返回一個數(shù)組。數(shù)組的第一個成員是狀態(tài)的當前值,第二個成員是發(fā)送 action 的dispatch函數(shù)。
下面是一個計數(shù)器的例子。用于計算狀態(tài)的 Reducer 函數(shù)如下。
const myReducer = (state, action) => {
switch(action.type) {
case('countUp'):
return {
...state,
count: state.count + 1
}
default:
return state;
}
}
組件代碼如下。
function App() {
const [state, dispatch] = useReducer(myReducer, { count: 0 });
return (
<div className="App">
<button onClick={() => dispatch({ type: 'countUp' })}>
+1
</button>
<p>Count: {state.count}</p>
</div>
);
}
由于 Hooks 可以提供共享狀態(tài)和 Reducer 函數(shù),所以它在這些方面可以取代 Redux。但是,它沒法提供中間件(middleware)和時間旅行(time travel),如果你需要這兩個功能,還是要用 Redux。
useEffect():副作用鉤子
useEffect()用來引入具有副作用的操作,最常見的就是向服務器請求數(shù)據(jù)。以前,放在componentDidMount里面的代碼,現(xiàn)在可以放在useEffect()。
useEffect()的用法如下。
useEffect(() => {
// Async Action
}, [dependencies])
上面用法中,useEffect()接受兩個參數(shù)。第一個參數(shù)是一個函數(shù),異步操作的代碼放在里面。第二個參數(shù)是一個數(shù)組,用于給出 Effect 的依賴項,只要這個數(shù)組發(fā)生變化,useEffect()就會執(zhí)行。第二個參數(shù)可以省略,這時每次組件渲染時,就會執(zhí)行useEffect()。
下面看一個例子。
const Person = ({ personId }) => {
const [loading, setLoading] = useState(true);
const [person, setPerson] = useState({});
useEffect(() => {
setLoading(true);
fetch(`https://swapi.co/api/people/${personId}/`)
.then(response => response.json())
.then(data => {
setPerson(data);
setLoading(false);
});
}, [personId])
if (loading === true) {
return <p>Loading ...</p>
}
return <div>
<p>You're viewing: {person.name}</p>
<p>Height: {person.height}</p>
<p>Mass: {person.mass}</p>
</div>
}
上面代碼中,每當組件參數(shù)personId發(fā)生變化,useEffect()就會執(zhí)行。組件第一次渲染時,useEffect()也會執(zhí)行。
useContext
如果需要在組件之間共享狀態(tài),可以使用useContext()。
現(xiàn)在有倆個組件Navbar和Messages,我們希望它們之間共享狀態(tài)。
<div className="test">
<Navbar />
<Messages />
</div>
使用方法如下:
第一步在它們的父組件上使用React的Context API,在組件外部建立一個Context。
const TestContext = React.createContext({)};
組件封裝代碼如下:
<TestContext.Provider
value={{
username: 'superawesome',
}}
>
<div className="test">
<Navbar />
<Messages />
</div>
<TestContext.Provider/>
上面的代碼中,TestContext.Provider提供了一個Context對象,這個對象是可以被子組件共享的。
Navbar組件的代碼如下:
const Navbar = () => {
const { username } = useContext(TestContext);
return (
<div className="navbar">
<p>{username}</p>
</div>
)
}
上面代碼中,useContext()鉤子函數(shù)用來引入Context對象,從中獲取username屬性。
Message組件的代碼也類似:
const Messages = () => {
const { username } = useContext(TestContext);
return (
<div className="messages">
<p>1 message for {username}</p>
</div>
)
}
整體代碼如下:
import React, { useContext } from "react";
import ReactDOM from "react-dom";
const TestContext= React.createContext({});
const Navbar = () => {
const { username } = useContext(TestContext)
return (
<div className="navbar">
<p>{username}</p>
</div>
)
}
const Messages = () => {
const { username } = useContext(TestContext)
return (
<div className="messages">
<p>1 message for {username}</p>
</div>
)
}
function App() {
return (
<TestContext.Provider
value={{
username: 'superawesome',
}}
>
<div className="test">
<Navbar />
<Messages />
</div>
<TestContext.Provider/>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useMemo
我們來看一個反例:
import React from 'react';
export default function WithoutMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
function expensive() {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}
return <div>
<h4>{count}-{val}-{expensive()}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</div>;
}
這里創(chuàng)建了兩個state,然后通過expensive函數(shù),執(zhí)行一次昂貴的計算,拿到count對應的某個值。我們可以看到:無論是修改count還是val,由于組件的重新渲染,都會觸發(fā)expensive的執(zhí)行(能夠在控制臺看到,即使修改val,也會打印);但是這里的昂貴計算只依賴于count的值,在val修改的時候,是沒有必要再次計算的。在這種情況下,我們就可以使用useMemo,只在count的值修改時,執(zhí)行expensive計算:
export default function WithMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
const expensive = useMemo(() => {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}, [count]);
return <div>
<h4>{count}-{expensive}</h4>
{val}
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</div>;
}
上面我們可以看到,使用useMemo來執(zhí)行昂貴的計算,然后將計算值返回,并且將count作為依賴值傳遞進去。這樣,就只會在count改變的時候觸發(fā)expensive執(zhí)行,在修改val的時候,返回上一次緩存的值。
useCallback
useCallback跟useMemo比較類似,但它返回的是緩存的函數(shù)。我們看一下最簡單的用法:
const fnA = useCallback(fnB, [a])
import React, { useState, useCallback } from 'react';
const set = new Set();
export default function Callback() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
console.log(count);
}, [count]);
set.add(callback);
return <div>
<h4>{count}</h4>
<h4>{set.size}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
我們可以看到,每次修改count,set.size就會+1,這說明useCallback依賴變量count,count變更時會返回新的函數(shù);而val變更時,set.size不會變,說明返回的是緩存的舊版本函數(shù)。
知道useCallback有什么樣的特點,那有什么作用呢?
使用場景是:有一個父組件,其中包含子組件,子組件接收一個函數(shù)作為props;通常而言,如果父組件更新了,子組件也會執(zhí)行更新;但是大多數(shù)場景下,更新是沒有必要的,我們可以借助useCallback來返回函數(shù),然后把這個函數(shù)作為props傳遞給子組件;這樣,子組件就能避免不必要的更新。
import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
return count;
}, [count]);
return <div>
<h4>{count}</h4>
<Child callback={callback}/>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
function Child({ callback }) {
const [count, setCount] = useState(() => callback());
useEffect(() => {
setCount(callback());
}, [callback]);
return <div>
{count}
</div>
}
不僅是上面的例子,所有依賴本地狀態(tài)或props來創(chuàng)建函數(shù),需要使用到緩存函數(shù)的地方,都是useCallback的應用場景。
多談一點
useEffect、useMemo、useCallback都是自帶閉包的。也就是說,每一次組件的渲染,其都會捕獲當前組件函數(shù)上下文中的狀態(tài)(state, props),所以每一次這三種hooks的執(zhí)行,反映的也都是當前的狀態(tài),你無法使用它們來捕獲上一次的狀態(tài)。對于這種情況,我們應該使用ref來訪問。
參考文章:
阮一峰老師:http://www.ruanyifeng.com/blog/2019/09/react-hooks.html
優(yōu)質(zhì)博客:
https://blog.csdn.net/sinat_17775997/article/details/94453167
http://www.itdecent.cn/p/1252be39c702
http://www.itdecent.cn/p/7e778adec7d1
http://www.itdecent.cn/p/d6244228a427