在學會使用React Hooks以前,能夠先看一下相關原理學習React Hooksreact
在 React 的世界中,有容器組件和 UI 組件之分,在 React Hooks 出現以前,UI 組件咱們可使用函數,無狀態組件來展現 UI,而對於容器組件,函數組件就顯得無能爲力,咱們依賴於類組件來獲取數據,處理數據,並向下傳遞參數給 UI 組件進行渲染。在我看來,使用 React Hooks 相比於從前的類組件有如下幾點好處:git
React 在 v16.8 的版本中推出了 React Hooks 新特性,雖然社區尚未最佳實踐如何基於 React Hooks 來打造複雜應用(至少我尚未),憑藉着閱讀社區中大量的關於這方面的文章,下面我將經過十個案例來幫助你認識理解並能夠熟練運用 React Hooks 大部分特性。github
在類組件中,咱們使用 this.state
來保存組件狀態,並對其修改觸發組件從新渲染。好比下面這個簡單的計數器組件,很好詮釋了類組件如何運行:在線 Demoredux
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>
);
}
}
複製代碼
一個簡單的計數器組件就完成了,而在函數組件中,因爲沒有 this 這個黑魔法,React 經過 useState 來幫咱們保存組件的狀態。在線 Demo數組
import React, { useState } from "react";
function App() {
const [obj, setObject] = useState({
count: 0,
name: "alife"
});
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 參數後返回一個帶有默認狀態和改變狀態函數的數組。經過傳入新狀態給函數來改變本來的狀態值。值得注意的是 useState 不幫助你處理狀態,相較於 setState 非覆蓋式更新狀態,useState 覆蓋式更新狀態,須要開發者本身處理邏輯。(代碼如上)bash
彷佛有個 useState 後,函數組件也能夠擁有本身的狀態了,但僅僅是這樣徹底不夠。markdown
函數組件能保存狀態,可是對於異步請求,反作用的操做仍是無能爲力,因此 React 提供了 useEffect 來幫助開發者處理函數組件的反作用,在介紹新 API 以前,咱們先來看看類組件是怎麼作的:在線 Demoapp
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>
);
}
}
複製代碼
在例子中,組件每隔一秒更新組件狀態,而且每次觸發更新都會觸發 document.title 的更新(反作用),而在組件卸載時修改 document.title(相似於清除)異步
從例子中能夠看到,一些重複的功能開發者須要在 componentDidMount 和 componentDidUpdate 重複編寫,而若是使用 useEffect 則徹底不同。在線 Demoide
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>
);
}
複製代碼
咱們使用 useEffect 重寫了上面的例子,useEffect 第一個參數接收一個函數,能夠用來作一些反作用好比異步請求,修改外部參數等行爲,而第二個參數稱之爲dependencies,是一個數組,若是數組中的值變化纔會觸發 執行useEffect 第一個參數中的函數。返回值(若是有)則在組件銷燬或者調用函數前調用。
基於這個強大 Hooks,咱們能夠模擬封裝出其餘生命週期函數,好比 componentDidUpdate 代碼十分簡單
function useUpdate(fn) {
// useRef 建立一個引用
const mounting = useRef(true);
useEffect(() => {
if (mounting.current) {
mounting.current = false;
} else {
fn();
}
});
}
複製代碼
如今咱們有了 useState 管理狀態,useEffect 處理反作用,異步邏輯,學會這兩招足以應對大部分類組件的使用場景。
上面介紹了 useState、useEffect 這兩個最基本的 API,接下來介紹的 useContext 是 React 幫你封裝好的,用來處理多層級傳遞數據的方式,在之前組件樹種,跨層級祖先組件想要給孫子組件傳遞數據的時候,除了一層層 props 往下透傳以外,咱們還可使用 React Context API 來幫咱們作這件事,舉個簡單的例子:在線 Demo
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>
);
}
複製代碼
經過 React createContext 的語法,在 APP 組件中能夠跨過 Foo 組件給 Bar 傳遞數據。而在 React Hooks 中,咱們可使用 useContext 進行改造。在線 Demo
const colorContext = React.createContext("gray");
function Bar() {
const color = useContext(colorContext);
return <div>{color}</div>;
}
function Foo() {
return <Bar />;
}
function App() {
return (
<colorContext.Provider value={"red"}>
<Foo />
</colorContext.Provider>
);
}
複製代碼
傳遞給 useContext 的是 context 而不是 consumer,返回值便是想要透傳的數據了。用法很簡單,使用 useContext 能夠解決 Consumer 多狀態嵌套的問題。參考例子
function HeaderBar() {
return (
<CurrentUser.Consumer>
{user =>
<Notifications.Consumer>
{notifications =>
<header>
Welcome back, {user.name}!
You have {notifications.length} notifications.
</header>
}
}
</CurrentUser.Consumer>
);
}
複製代碼
而使用 useContext 則變得十分簡潔,可讀性更強且不會增長組件樹深度。
function HeaderBar() {
const user = useContext(CurrentUser);
const notifications = useContext(Notifications);
return (
<header>
Welcome back, {user.name}!
You have {notifications.length} notifications.
</header>
);
}
複製代碼
useReducer 這個 Hooks 在使用上幾乎跟 Redux/React-Redux 如出一轍,惟一缺乏的就是沒法使用 redux 提供的中間件。咱們將上述的計時器組件改寫爲 useReducer,在線 Demo
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>
</>
);
}
複製代碼
用法跟 Redux 基本上是一致的,用法也很簡單,算是提供一個 mini 的 Redux 版本。
在類組件中,咱們常常犯下面這樣的錯誤:
class App {
render() {
return <div>
<SomeComponent style={{ fontSize: 14 }} doSomething={ () => { console.log('do something'); }} />
</div>;
}
}
複製代碼
這樣寫有什麼壞處呢?一旦 App 組件的 props 或者狀態改變了就會觸發重渲染,即便跟 SomeComponent 組件不相關,因爲每次 render 都會產生新的 style 和 doSomething(由於從新render先後, style 和 doSomething分別指向了不一樣的引用),因此會致使 SomeComponent 從新渲染,假若 SomeComponent 是一個大型的組件樹,這樣的 Virtual Dom 的比較顯然是很浪費的,解決的辦法也很簡單,將參數抽離成變量。
const fontSizeStyle = { fontSize: 14 };
class App {
doSomething = () => {
console.log('do something');
}
render() {
return <div>
<SomeComponent style={fontSizeStyle} doSomething={ this.doSomething } />
</div>;
}
}
複製代碼
在類組件中,咱們還能夠經過 this 這個對象來存儲函數,而在函數組件中沒辦法進行掛載了。因此函數組件在每次渲染的時候若是有傳遞函數的話都會重渲染子組件。
function App() {
const handleClick = () => {
console.log('Click happened');
}
return <SomeComponent onClick={handleClick}>Click Me</SomeComponent>;
}
複製代碼
這裏多說一句,一版把函數式組件理解爲class組件render函數的語法糖,因此每次從新渲染的時候,函數式組件內部全部的代碼都會從新執行一遍。因此上述代碼中每次render,handleClick都會是一個新的引用,因此也就是說傳遞給SomeComponent組件的props.onClick一直在變(由於每次都是一個新的引用),因此纔會說這種狀況下,函數組件在每次渲染的時候若是有傳遞函數的話都會重渲染子組件。
而有了 useCallback 就不同了,你能夠經過 useCallback 得到一個記憶後的函數。
function App() {
const memoizedHandleClick = useCallback(() => {
console.log('Click happened')
}, []); // 空數組表明不管什麼狀況下該函數都不會發生改變
return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>;
}
複製代碼
老規矩,第二個參數傳入一個數組,數組中的每一項一旦值或者引用發生改變,useCallback 就會從新返回一個新的記憶函數提供給後面進行渲染。
這樣只要子組件繼承了 PureComponent 或者使用 React.memo 就能夠有效避免沒必要要的 VDOM 渲染。
useCallback 的功能徹底能夠由 useMemo 所取代,若是你想經過使用 useMemo 返回一個記憶函數也是徹底能夠的。
useCallback(fn, inputs) is equivalent to useMemo(() => fn, inputs).
複製代碼
因此前面使用 useCallback 的例子可使用 useMemo 進行改寫:
function App() {
const memoizedHandleClick = useMemo(() => () => {
console.log('Click happened')
}, []); // 空數組表明不管什麼狀況下該函數都不會發生改變
return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>;
}
複製代碼
惟一的區別是:**useCallback 不會執行第一個參數函數,而是將它返回給你,而 useMemo 會執行第一個函數而且將函數執行結果返回給你。**因此在前面的例子中,能夠返回 handleClick 來達到存儲函數的目的。
因此 useCallback 經常使用記憶事件函數,生成記憶後的事件函數並傳遞給子組件使用。而 useMemo 更適合通過函數計算獲得一個肯定的值,好比記憶組件。
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}
複製代碼
當 a/b 改變時,child1/child2 纔會從新渲染。從例子能夠看出來,只有在第二個參數數組的值發生變化時,纔會觸發子組件的更新。
useRef 跟 createRef 相似,均可以用來生成對 DOM 對象的引用,看個簡單的例子:在線 Demo
import React, { useState, useRef } from "react";
function App() {
let [name, setName] = useState("Nate");
let nameRef = useRef();
const submitButton = () => {
setName(nameRef.current.value);
};
return (
<div className="App">
<p>{name}</p>
<div>
<input ref={nameRef} type="text" />
<button type="button" onClick={submitButton}>
Submit
</button>
</div>
</div>
);
}
複製代碼
useRef 返回的值傳遞給組件或者 DOM 的 ref 屬性,就能夠經過 ref.current 值訪問組件或真實的 DOM 節點,重點是組件也是能夠訪問到的,從而能夠對 DOM 進行一些操做,好比監聽事件等等。
固然 useRef 遠比你想象中的功能更增強大,useRef 的功能有點像類屬性,或者說您想要在組件中記錄一些值,而且這些值在稍後能夠更改。
利用 useRef 就能夠繞過 Capture Value 的特性。能夠認爲 ref 在全部 Render 過程當中保持着惟一引用,所以全部對 ref 的賦值或取值,拿到的都只有一個最終狀態,而不會在每一個 Render 間存在隔離。參考例子:精讀《Function VS Class 組件》
React Hooks 中存在 Capture Value 的特性:在線 Demo
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
alert("count: " + count);
}, 3000);
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>增長 count</button>
<button onClick={() => setCount(count - 1)}>減小 count</button>
</div>
);
}
複製代碼
先點擊增長button,後點擊減小button,3秒後先alert 1,後alert 0,而不是alert兩次0。這就是所謂的 capture value 的特性。而在類組件中 3 秒後輸出的就是修改後的值,由於這時候** message 是掛載在 this 變量上,它保留的是一個引用值**,對 this 屬性的訪問都會獲取到最新的值,類組件舉例,在線Demo。講到這裏你應該就明白了,useRef 建立一個引用,就能夠有效規避 React Hooks 中 Capture Value 特性。useRef避免 Capture Value 在線Demo
function App() {
const count = useRef(0);
const showCount = () => {
alert("count: " + count.current);
};
const handleClick = number => {
count.current = count.current + number;
setTimeout(showCount, 3000);
};
return (
<div>
<p>You clicked {count.current} times</p>
<button onClick={() => handleClick(1)}>增長 count</button>
<button onClick={() => handleClick(-1)}>減小 count</button>
</div>
);
}
複製代碼
只要將賦值與取值的對象變成 useRef,而不是 useState,就能夠躲過 capture value 特性,在 3 秒後獲得最新的值。
經過 useImperativeHandle 用於讓父組件獲取子組件內的索引 在線 Demo
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>
);
}
複製代碼
經過這種方式,App 組件能夠得到子組件的 input 的 DOM 節點。
大部分狀況下,使用 useEffect 就能夠幫咱們處理組件的反作用,可是若是想要同步調用一些反作用,好比對 DOM 的操做,就須要使用 useLayoutEffect,useLayoutEffect 中的反作用會在 DOM 更新以後同步執行。在線 Demo
function App() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
const title = document.querySelector("#title");
const titleWidth = title.getBoundingClientRect().width;
console.log("useLayoutEffect");
if (width !== titleWidth) {
setWidth(titleWidth);
}
});
useEffect(() => {
console.log("useEffect");
});
return (
<div>
<h1 id="title">hello</h1>
<h2>{width}</h2>
</div>
);
}
複製代碼
在上面的例子中,useLayoutEffect 會在 render,DOM 更新以後同步觸發函數,會優於 useEffect 異步觸發函數。
簡單來講就是調用時機不一樣,useLayoutEffect
和原來componentDidMount
&componentDidUpdate
一致,在react完成DOM更新後立刻同步調用的代碼,會阻塞頁面渲染。而useEffect
是會在整個頁面渲染完纔會調用的代碼。
官方建議優先使用useEffect
However, we recommend starting with useEffect first and only trying useLayoutEffect if that causes a problem.
在實際使用時若是想避免頁面抖動(在useEffect
裏修改DOM頗有可能出現)的話,能夠把須要操做DOM的代碼放在useLayoutEffect
裏。關於使用useEffect
致使頁面抖動,參考git倉庫git倉庫示例
不過useLayoutEffect
在服務端渲染時會出現一個warning,要消除的話得用useEffect
代替或者推遲渲染時機。見說明和討論。
儘管咱們經過上面的例子看到 React Hooks 的強大之處,彷佛類組件徹底均可以使用 React Hooks 重寫。可是當下 v16.8 的版本中,還沒法實現 getSnapshotBeforeUpdate 和 componentDidCatch 這兩個在類組件中的生命週期函數。官方也計劃在不久的未來在 React Hooks 進行實現。