[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的思维方式