函式元件,沒有 class 元件中的 componentDidMount、componentDidUpdate 等生命週期方法,也沒有 State,但這些可以通過 React Hook 實現。
React Hooks 的意思是,元件儘量寫成純函式,如果需要外部功能和副作用,就用鉤子把外部程式碼"鉤"進來
React Hooks優點
- 程式碼可讀性更強:通過 React Hooks 可以將功能程式碼聚合,方便閱讀維護
- 元件樹層級變淺:在原本的程式碼中,我們經常使用 HOC/render props 等方式來複用元件的狀態,增強功能等,無疑增加了元件樹層數及渲染,而在 React Hooks 中,這些功能都可以通過強大的自定義的 Hooks 來實現
九種常用的鉤子
- useState:儲存元件狀態
- useEffect: 處理副作用
- useContext: 減少元件層級
- useReducer:類似於redux,通訊
- useCallback: 記憶函式
- useMemo: 記憶元件
- useRef: 儲存引用值
- useImperativeHandle: 透傳 Ref
- useLayoutEffect: 同步執行副作用
1、useState儲存元件狀態
用來代替:state,setState
若使用物件做 State,useState 更新時會直接替換掉它的值,而不像 setState 一樣把更新的欄位合併進物件中。推薦將 State 物件分成多個 State 變數。
類元件案例
在類元件中,我們使用 this.state
來儲存元件狀態,並對其修改觸發元件重新渲染。
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
name: "alife"
};
}
render() {
const { count } = this.state;
return (
<div>
Count: {count}
<button onClick={() => this.setState({ count: count + 1 })}>+</button>
<button onClick={() => this.setState({ count: count - 1 })}>-</button>
</div>
);
}
}
函式元件useState
在函式元件中,由於沒有 this 這個黑魔法,React 通過 useState 來幫我們儲存元件的狀態。
- 通過傳入 useState 引數後返回一個帶有預設狀態和改變狀態函式的陣列[obj, setObject] (名稱是自定義的可修改[count, setCount]...)。
- 通過傳入 新狀態給函式 來改變原本的狀態值。
import React, { useState } from "react";
function App() {
const [obj, setObject] = useState({
count: 0,
name: "alife"
});
let [count, setCount] = useState(0)
return (
<div className="App">
Count: {obj.count}
<button onClick={() => setObject({ ...obj, count: obj.count + 1 })}>+</button>
<button onClick={() => setObject({ ...obj, count: obj.count - 1 })}>-</button>
</div>
);
}
注意: useState 不幫助你處理狀態,相較於 setState 非覆蓋式更新狀態,useState 覆蓋式更新狀態,需要開發者自己處理邏輯。(程式碼如上)
2、useEffect 處理副作用
用來代替:componentDidMount、componentDidUpdate和componentWillUnmount的組合體。
預設情況下,useEffect會在第一次渲染之後和每次更新之後執行,每次執行useEffect時,DOM 已經更新完畢。
為了控制useEffect的執行時機與次數,可以使用第二個可選引數施加控制。
類元件案例
在例子中,元件每隔一秒更新元件狀態,並且每次觸發更新都會觸發 document.title 的更新(副作用),而在元件解除安裝時修改 document.title(類似於清除)
從例子中可以看到,一些重複的功能開發者需要在 componentDidMount 和 componentDidUpdate 重複編寫,而如果使用 useEffect 則完全不一樣。
import React, { Component } from "react";
class App extends Component {
state = {
count: 1
};
componentDidMount() {
const { count } = this.state;
document.title = "componentDidMount" + count;
this.timer = setInterval(() => {
this.setState(({ count }) => ({
count: count + 1
}));
}, 1000);
}
componentDidUpdate() {
const { count } = this.state;
document.title = "componentDidMount" + count;
}
componentWillUnmount() {
document.title = "componentWillUnmount";
clearInterval(this.timer);
}
render() {
const { count } = this.state;
return (
<div>
Count:{count}
<button onClick={() => clearInterval(this.timer)}>clear</button>
</div>
);
}
}
useEffect
引數1:接收一個函式,可以用來做一些副作用比如非同步請求,修改外部引數等行為。
引數2:稱之為dependencies,是一個陣列,如果陣列中的值變化才會觸發 執行useEffect 第一個引數中的函式。返回值(如果有)則在元件銷燬或者呼叫函式前呼叫
-
- 比如第一個 useEffect 中,理解起來就是一旦 count 值發生改變,則修改 documen.title 值;
-
- 而第二個 useEffect 中傳遞了一個空陣列[],這種情況下只有在元件初始化或銷燬的時候才會觸發,用來代替 componentDidMount 和 componentWillUnmount,慎用;
-
- 還有另外一個情況,就是不傳遞第二個引數,也就是useEffect只接收了第一個函式引數,代表不監聽任何引數變化。每次渲染DOM之後,都會執行useEffect中的函式。
import React, { useState, useEffect } from "react";
let timer = null;
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = "componentDidMount" + count;
},[count]);
useEffect(() => {
timer = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 一定注意下這個順序:
// 告訴react在下次重新渲染元件之後,同時是下次執行上面setInterval之前呼叫
return () => {
document.title = "componentWillUnmount";
clearInterval(timer);
};
}, []);
return (
<div>
Count: {count}
<button onClick={() => clearInterval(timer)}>clear</button>
</div>
);
}
3、useContext 減少元件層級
用來代替:Provider, Consumer
處理多層級傳遞資料的方式,在以前元件樹種,跨層級祖先元件想要給孫子元件傳遞資料的時候,除了一層層 props 往下透傳之外,我們還可以使用 React Context API 來幫我們做這件事
類元件案例
const { Provider, Consumer } = React.createContext(null);
function Bar() {
return <Consumer>{color => <div>{color}</div>}</Consumer>;
}
function Foo() {
return <Bar />;
}
function App() {
return (
<Provider value={"grey"}>
<Foo />
</Provider>
);
}
useContext使用的方法
- 要先建立createContex
使用createContext建立並初始化import { createContext } from 'react' const C = createContext(null);
- Provider 指定使用的範圍
在圈定的範圍內,傳入讀操作和寫操作物件,然後可以使用上下文<C.Provider value={{n,setN}}> 這是爺爺 <Baba></Baba> </C.Provider>
- 最後使用useContext
使用useContext接受上下文,因為傳入的是物件,則接受的也應該是物件const {n,setN} = useContext(C);
import React, { createContext, useContext, useReducer, useState } from 'react'
import ReactDOM from 'react-dom'
// 創造一個上下文
const C = createContext(null);
function App(){
const [n,setN] = useState(0)
return(
// 指定上下文使用範圍,使用provider,並傳入讀資料和寫入據
<C.Provider value={{n,setN}}>
這是爺爺
<Baba></Baba>
</C.Provider>
)
}
function Baba(){
return(
<div>
這是爸爸
<Child></Child>
</div>
)
}
function Child(){
// 使用上下文,因為傳入的是物件,則接受也應該是物件
const {n,setN} = useContext(C)
const add=()=>{
setN(n=>n+1)
};
return(
<div>
這是兒子:n:{n}
<button onClick={add}>+1</button>
</div>
)
}
ReactDOM.render(<App />,document.getElementById('root'));
4、useReducer 資料互動
用來代替:Redux/React-Redux
唯一缺少的就是無法使用 redux 提供的中介軟體
import React, { useReducer } from "react";
const initialState = {
count: 0
};
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + action.payload };
case "decrement":
return { count: state.count - action.payload };
default:
throw new Error();
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "increment", payload: 5 })}>
+
</button>
<button onClick={() => dispatch({ type: "decrement", payload: 5 })}>
-
</button>
</>
);
}
5、useCallback 記憶函式
減少重複渲染
老規矩,第二個引數傳入一個陣列,陣列中的每一項一旦值或者引用發生改變,useCallback 就會重新返回一個新的記憶函式提供給後面進行渲染。
這樣只要子元件繼承了 PureComponent 或者使用 React.memo 就可以有效避免不必要的 VDOM 渲染。
import React, { useState, useCallback, memo } from 'react'
const Child = memo(function(props) {
console.log('child run...')
return (
<>
<h1>hello</h1>
<button onClick={props.onAdd}>add</button>
</>
)
})
export default function UseCallback() {
console.log('parent run...')
let [ count, setCount ] = useState(0)
const handleAdd = useCallback(
() => {
console.log('added.')
},
[],
)
return (
<div>
<div>{count}</div>
<Child onAdd={handleAdd}></Child>
<button onClick={() => setCount(100)}>change count</button>
</div>
)
}
6、useMemo 記憶元件
useCallback 的功能完全可以由 useMemo 所取代,如果你想通過使用 useMemo 返回一個記憶函式也是完全可以的。
區別:useCallback 不會執行第一個引數函式,而是將它返回給你,而 useMemo 會執行第一個函式並且將函式執行結果返回給你
function App() { const memoizedHandleClick = useMemo(() => () => { console.log('Click happened') }, []); // 空陣列代表無論什麼情況下該函式都不會發生改變 return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>; }
- useCallback: 常用記憶事件函式,生成記憶後的事件函式並傳遞給子元件使用。
- useMemo: 更適合經過函式計算得到一個確定的值,比如記憶元件。
function Parent({ a, b }) { const child1 = useMemo(() => () => <Child1 a={a} />, [a]); const child2 = useMemo(() => () => <Child2 b={b} />, [b]); return ( <> {child1} {child2} </> ) }
7、useRef 儲存引用值
用來代替:createRef
Ref
React提供了一個屬性ref,用於表示對組價例項的引用,其實就是ReactDOM.render()返回的元件例項:
- ReactDOM.render()渲染元件時返回的是元件例項;
- 渲染dom元素時,返回是具體的dom節點。
ref可以掛載到元件上也可以是dom元素上;
- 掛到元件(class宣告的元件)上的ref表示對元件例項的引用。不能在函式式元件上使用 ref 屬性,因為它們沒有例項:
- 掛載到dom元素上時表示具體的dom元素節點。
useRef()
useRef這個hooks函式,除了傳統的用法之外,它還可以“跨渲染週期”儲存資料。
- 可以通過 ref.current 值訪問元件或真實的 DOM 節點,從而可以對 DOM 進行一些操作,比如監聽事件等等
export default function App() { const count = useRef(0) <!-- count.current儲存狀態值 --> const handleClick = (num) => { count.current += num setTimeout(() => { console.log("count: " + count.current); }, 3000) } return ( <div> <p>You clicked {count.current} times</p> <button onClick={() => handleClick(1)}>增加 count</button> <button onClick={() => handleClick(-1)}>減少 count</button> </div> ); }
8、useImperativeHandle 透傳 Ref
通過 useImperativeHandle 用於讓父元件獲取子元件內的索引
useImperativeHandle 應當與 forwardRef 一起使用
useImperativeHandle(ref, createHandle, [deps])
- ref:定義 current 物件的 ref createHandle:一個函式,返回值是一個物件,即這個 ref 的 current
- [deps]:即依賴列表,當監聽的依賴發生變化,useImperativeHandle 才會重新將子元件的例項屬性輸出到父元件
- 注意:ref 的 current 屬性上,如果為空陣列,則不會重新輸出。
import React, { useRef, useEffect, useImperativeHandle, forwardRef } from "react";
function ChildInputComponent(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => inputRef.current);
return <input type="text" name="child input" ref={inputRef} />;
}
const ChildInput = forwardRef(ChildInputComponent);
function App() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<div>
<ChildInput ref={inputRef} />
</div>
);
}
export default App
9、useLayoutEffect 同步執行副作用
大部分情況下,使用 useEffect 就可以幫我們處理元件的副作用,但是如果想要同步呼叫一些副作用,比如對 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用會在 DOM 更新之後同步執行。
(1) useEffect和useLayoutEffect有什麼區別?
簡單來說就是呼叫時機不同
useLayoutEffect
:和原來componentDidMount
&componentDidUpdate
一致,在react完成DOM更新後馬上同步呼叫的程式碼,會阻塞頁面渲染。useEffect
:是會在整個頁面渲染完才會呼叫的程式碼。- 官方建議優先使用
useEffect
在實際使用時如果想避免頁面抖動(在useEffect
裡修改DOM很有可能出現)的話,可以把需要操作DOM的程式碼放在useLayoutEffect
裡。關於使用useEffect
導致頁面抖動。
不過useLayoutEffect
在服務端渲染時會出現一個warning,要消除的話得用useEffect
代替或者推遲渲染時機。
import React from 'react'
import { useState, useEffect } from 'react'
export default function (props) {
let [data, setData] = useState({count: 0})
function loadData() {
return fetch('http://localhost:8080/api/movies/list')
.then(response => response.json())
.then(result => {
return result
})
}
useEffect(() => {
console.log('effect')
}, [data])
useEffect(() => {
console.log('mounted.')
;(async ()=>{
let result = await loadData()
console.log(result)
})()
// return () => {
// console.log('unmout')
// }
}, [])
return (
<>
<div>{data.count}</div>
<button onClick={() => setData(data => ({count: data.count + 1}))}>click</button>
</>
)
}