[TOC]
# 介紹
React 是主流的前端框架,v16.8 版本引入了全新的 API,叫做 [React Hooks](https://zh-hans.reactjs.org/docs/hooks-reference.html),顛覆了以前的用法。
如果你是 Hook 初學者,建議先閱讀 [https://usehooks.com/](https://usehooks.com/) 以及 [Dan Abramov 的個人博客](https://overreacted.io/)。
axios 以及 immer 等庫都未能 “幸免”,被 Hook 包裹了一層而變成了 [axios-hooks](https://github.com/simoneb/axios-hooks) 以及 [use-immer](https://github.com/immerjs/use-immer)。
# 思想
React 團隊希望,組件不要變成復雜的容器,最好只是數據流的管道。開發者根據需要,組合管道即可。**組件的最佳寫法應該是函數,而不是類。**
React 的函數組件,有重大限制:
1. 必須是純函數
2. 不能包含狀態
3. 也不支持生命周期方法
4. 因此無法取代類(沒有 this)
**React Hooks 的設計目的,就是加強版函數組件,組件盡量寫成純函數,如果需要外部功能和副作用,就用鉤子把外部代碼 "鉤" 進來。**
React Hooks 就是那些鉤子。
# 鉤子函數
React 為我們提供的鉤子:
* [Basic Hooks](https://zh-hans.reactjs.org/docs/hooks-reference.html#basic-hooks)
* [`useState`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usestate)
* [`useEffect`](https://zh-hans.reactjs.org/docs/hooks-reference.html#useeffect)
* [`useContext`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usecontext)
* [Additional Hooks](https://zh-hans.reactjs.org/docs/hooks-reference.html#additional-hooks)
* [`useReducer`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usereducer)
* [`useCallback`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usecallback)
* [`useMemo`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usememo)
* [`useRef`](https://zh-hans.reactjs.org/docs/hooks-reference.html#useref)
* [`useImperativeHandle`](https://zh-hans.reactjs.org/docs/hooks-reference.html#useimperativehandle)
* [`useLayoutEffect`](https://zh-hans.reactjs.org/docs/hooks-reference.html#uselayouteffect)
* [`useDebugValue`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usedebugvalue)
# `useState ()`
在初始渲染期間,返回的狀態 (`state`) 與傳入的第一個參數 (`initialState`) 值相同。
在后續的重新渲染中,`useState`返回的第一個值將始終是更新后最新的 state。
```
function Counter({initialCount}) {
// 返回一個數組
const [count, setCount] = useState(initialCount);
// Declare multiple state variables!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(c=> c + 1)}>+</button>
</>
);
}
```
**第二個成員**是一個函數,用來更新狀態,約定是`set`前綴加上狀態的變量名(上例是`setButtonText`)。
> [2020要用immer來代替immutable優化你的React項目](https://blog.csdn.net/GetIdea/article/details/103770851)
小結:
* React 會在重復渲染時保留 state。
* `useState`會返回一對值:**當前**狀態和一個讓你更新它的函數,你可以在事件處理函數中或其他一些地方調用這個函數。
* 更新函數 類似 class 組件的`this.setState`,但是它不會把新的 state 和舊的 state 進行合并。
* `setCount(c => c + 1)` // ? 在這不依賴于外部的 `count` 變量,每次`setCount`內部的回調取到的`count`是最新值(在回調中變量命名為`c`)。
# `useEffect ()`
默認情況下,第一次渲染之后和每次更新之后會運行 `useEffect()`。
effect 的全稱應該是 Side Effect,中文名叫副作用,我們在前端開發中常見的副作用有:
* dom 操作
* 瀏覽器事件綁定和取消綁定
* 發送 HTTP 請求
* 打印日志
* 訪問系統狀態
* 執行 IO 變更操作
示例:
```
// 添加了 數組項 personId 數據源依賴項時,根據該數據源是否變化來決定是否觸發回調
useEffect(() => {
setLoading(true);
fetch(`https://swapi.co/api/people/${personId}/`)
.then(response => response.json())
.then(data => {
setPerson(data);
setLoading(false);
});
}, [personId])
```
* 可以返回一個函數,來進行額外的[清理副作用](https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup)。(類似類組件的 `componentWillUnmount` 時觸發)
```
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清理訂閱
subscription.unsubscribe();
};
});
```
* 可以將 `useEffect` Hook 看作 `componentDidMount`,`componentDidUpdate` 和`componentWillUnmount` 的組合。
整個組件的生命周期流程可以這么理解:
> **組件掛載** --> 執行副作用 --> **組件更新** --> 執行清理函數 --> 執行副作用 --> ... --> **組件卸載**
小結:
* React 會等待瀏覽器完成畫面渲染之后才會延遲調用`useEffect`,因此會使得額外操作很方便
* effect 的清除階段在每次重新渲染時都會執行,而不是只在卸載組件的時候執行一次
* `useEffect`會在調用一個新的 effect 之前對前一個 effect 進行清理
* React 將按照 effect 聲明的順序依次調用組件中的每一個 effect
* 如果想執行**只運行一次的 effect(僅在組件掛載/卸載(mount/unmount)時執行),可以傳遞一個空數組(`[]`)作為第二個參數**。這就告訴 React 你的 effect 不依賴于 `props` 或 `state` 中的任何值,所以它永遠都不需要重復執行。這并不屬于特殊情況 —— 它依然遵循輸入數組的工作方式。
# `useContext ()`
1. 使用 React Context API,在組件外部建立一個 Context。
```
import { createContext } from "react";
const AppContext =createContext();
```
組件封裝代碼如下:
```
<AppContext.Provider value={{
username: 'superawesome'
}}>
<div className="App">
<Navbar/>
<Messages/>
</div>
</AppContext.Provider>
```
上面代碼中,`AppContext.Provider`提供了一個 Context 對象,這個對象可以被子組件共享。
2. 子組件需要 `useContext()` 鉤子函數用來引入 Context 對象,從中獲取 `username`屬性。
```
const Navbar = () => {
const { username } = useContext(AppContext);
return (
<div className="navbar">
<p>AwesomeSite</p>
<p>{username}</p>
</div>
);
}
```
# `useReducer ()`
Redux 的 Reducer 函數的形式是 `(state, action) => newState`。
`useReducer()` 鉤子用來引入 Reducer 功能。
(??:真不愧是 Redux 作者啊,還是用的 redux 的方式)
> React 會確保`dispatch`函數的標識是穩定的,并且不會在組件重新渲染時改變。這就是為什么可以安全地從`useEffect`或`useCallback`的依賴列表中省略`dispatch`。
```
import React, { useReducer } from "react";
...
// 定義 reducer
const myReducer = (state, action) => {
switch(action.type) {
case('countUp'):
return { ...state, count: state.count + 1 }
default:
return state
}
}
function App() {
// 參數為:reducer 和初始狀態,返回的數組為:更新后的狀態和 dispatch 用來分發狀態。
const [state, dispatch] = useReducer(myReducer, { count: 0 });
return (
<div className="App"> <button onClick={() => dispatch({ type: 'countUp' })}> +1 </button> <p>Count: {state.count}</p> </div>
);
}
...
```
# `useRef()`
[`useRef()`](https://zh-hans.reactjs.org/docs/hooks-reference.html#useref)Hook 不僅可以用于 DOM refs。「ref」 對象是具有一個可變`current`屬性且可以容納任意值的通用容器,類似于一個 class 的實例屬性
```
// initialValue 可有可無,或者設置為 null
const savedCallback = useRef(initialValue);
function callback() {
setCount(count + 1);
}
useEffect(() => {
// 可以賦任意值到 current 屬性中,進行保存
savedCallback.current = callback;
});
```
`useRef()` 返回**一個帶有 `current` 可變屬性(初始值為`initialValue`)的普通對象**在每一次渲染之間共享。
請記住,`useRef` 的內容更改時不會通知您。 更改 `current` 屬性不會導致重新渲染。
# `useCallback` 與 `useMemo`
1. `useCallback` 返回一個[`memoized`](https://en.wikipedia.org/wiki/Memoization)回調函數。
2. `useMemo` 返回一個 任何值,參數:一個具有返回?value?的函數、依賴項數組)。
如果沒有提供依賴項數組,`useMemo`在每次渲染時都會計算新的值。
> `useCallback(fn, deps)`相當于`useMemo(() => fn, deps)`。
優化示例:
```
...
function Foo({bar, baz}) {
useEffect(() => {
const options = {bar, baz}
buzz(options)
}, [bar, baz])
return <div>foobar</div>
}
function Blub() {
const bar = useCallback(() => {}, [])
// 傳遞了`[]`作為`useCallback`的依賴列表。這確保了 callback 不會在再次渲染時改變,因此 React 不會在非必要的時候調用它。
const baz = useMemo(() => [1, 2, 3], [])
// 沒有提供依賴項數組,`useMemo`在每次渲染時都會計算新的值
return <Foo bar={bar} baz={baz} />
}
...
```
小結:
* `useMemo`是在渲染期間執行的,所以`useMemo`中不要執行一些有副作用的操作。
* 沒有提供依賴項數組,`useMemo`在每次渲染時都會計算新的值,依賴數組是空數組的話,`useMemo`中的函數就只會執行一次。
* 在實現?useMemo?時,你需要問問自己:“這真的是一個代價高昂的函數嗎?”?代價高昂意味著它正在消耗大量資源(如內存)。如果在渲染時在函數中定義大量變量,則用?useMemo?進行記憶是非常有意義的。
> [什么時候使用 useMemo 和 useCallback-依賴列表](https://jancat.github.io/post/2019/translation-usememo-and-usecallback/#%E4%BE%9D%E8%B5%96%E5%88%97%E8%A1%A8)
# Custom Hook
就是根據業務場景對以上四種 Hooks 進行組裝,從而得到滿足自己需求的鉤子。
1. 一個 JavaScript 函數
2. 名稱以`use` 開頭
3. 可以調用其他鉤子
> 自定義 hook 是一種自然遵循于 hook 設計的約定,而不是一個 React 特性
> 自定義 hook 沒有特別的語法,就是利用:`state`的改變會引發函數組件重新執行這一特性!
比如,我們要將我們上面的代碼功能封裝成 Hooks, 代碼如下
```
import React, { useState, useEffect } from 'react'
// 自定義的 Hook
const usePerson = (name) => {
const [loading, setLoading] = useState(true)
const [person, setPerson] = useState({})
useEffect(() => {
setLoading(true)
setTimeout(()=> {
setLoading(false)
setPerson({name})
},2000)
},[name])
return [loading,person]
}
const AsyncPage = ({name}) => {
const [loading, person] = usePerson(name)
return (
<>
{loading?<p>Loading...</p>:<p>{person.name}</p>}
</>
)
}
const PersonPage = () =>{
const [state, setState]=useState('')
const changeName = (name) => {
setState(name)
}
return (
<>
<AsyncPage name={state}/>
<button onClick={() => {changeName('名字1')}}>名字1</button>
<button onClick={() => {changeName('名字2')}}>名字2</button>
</>
)
}
export default PersonPage
```
上面代碼中,我們將之前的例子封裝成了自己的 Hooks, 便于共享。其中,我們定義 `usePerson()` 為我們的自定義 Hooks,它接受一個字符串,返回一個數組,數組中包括兩個數據的狀態,之后我們在使用 `usePerson()` 時,會根據我們傳入的參數不同而返回不同的狀態(原理),然后很簡便的應用于我們的頁面中。
# 思考??
不要在循環,條件或嵌套函數中調用 Hook。
相反,請始終在您的 React 函數的頂層使用 Hook。
通過遵循此規則,可以確保每次渲染組件時都以相同的順序調用 Hook。
這些是讓 React 在多個`useState`和`useEffect`調用之間正確保留 Hook 的狀態的原因。
> [https://reactjs.org/docs/hooks-rules.html#explanation](https://reactjs.org/docs/hooks-rules.html#explanation)
> * **完全可選的。** 你無需重寫任何已有代碼就可以在一些組件中嘗試 Hook。但是如果你不想,你不必現在就去學習或使用 Hook。
> * **100% 向后兼容的。** Hook 不包含任何破壞性改動。
> * **現在可用。** Hook 已發布于 v16.8.0。
> * **沒有計劃從 React 中移除 class。**
> * **Hook 不會影響你對 React 概念的理解。** 恰恰相反,Hook 為已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。
使用 React Hook 規則: https://zh-hans.reactjs.org/docs/hooks-rules.html
使用 ESlint 插件: https://www.npmjs.com/package/eslint-plugin-react-hooks
# 常見問題
[setInterval() 與 Hooks](https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks/)
[Writing Redux-like simple middleware for React Hooks](https://medium.com/front-end-weekly/writing-redux-like-simple-middleware-for-react-hooks-b163724a7058)
# 相關庫
[Awesome-React-Hooks](https://github.com/rehooks/awesome-react-hooks)
## Form
[React Hook Form](https://react-hook-form.com/)
Performant, flexible and extensible forms with easy-to-use validation
## React Hooks 庫
[usehooks.com](https://usehooks.com/)
[react-query](https://github.com/tannerlinsley/react-query)
[SWR](https://github.com/zeit/swr)
[react-use](https://github.com/streamich/react-use)
[豐富的 Hooks](https://github.com/umijs/hooks)
[React Custom Hooks 最佳實踐](https://github.com/brickspert/blog/issues/31)
# 參考
超性感的react hooks(六)自定義hooks的思維方式