一、Hook 简介
1.1 Hook 历史
在 React Hook 呈现之前的版本中,组件次要分为两种:函数式组件和类组件。其中,函数式组件通常只思考负责 UI 的渲染,没有本身的状态也没有业务逻辑代码,是一个纯函数。而类组件则不同,类组件有本人的外部状态,界面的显示后果通常由 props 和 state 决定,因而它也不再那么纯净。函数式组件,类组件有如下一些毛病:
- 状态逻辑难以复用。在类组件中,为了重用某些状态逻辑,社区提出了 render props 或者 hoc 等计划,然而这些计划对组件的侵入性太强,并且组件嵌套还容易造成嵌套天堂的问题。
- 滥用组件状态。大多数开发者在编写组件时,不论这个组件有木有外部状态,会不会执行生命周期函数,都会将组件编写成类组件,这造成不必要的性能开销。
- 额定的工作解决。应用类组件开发利用时,须要开发者额定去关注 this、事件监听器的增加和移除等等问题。
在函数式组件大行其道的以后,类组件正在逐步被淘汰。不过,函数式组件也并非毫无毛病,在之前的写法中,想要治理函数式组件状态共享就是比拟麻烦的问题。例如,上面这个函数组件就是一个纯函数,它的输入只由参数 props 决定,不受其余任何因素影响。
function App(props) {const {name, age} = props.info
return (<div style={{ height: '100%'}}>
<h1>Hello,i am ({name}),and i am ({age}) old</h1>
</div>
)
}
在下面的函数式组件中,一旦咱们须要给组件加状态,那就只能将组件重写为类组件,因为函数组件没有实例,没有生命周期。所以咱们说在 Hook 之前的函数组件和类组件最大的区别其实就是状态的有无。
1.2 Hook 概览
为了解决函数式组件状态的问题,React 在 16.8 版本新增了 Hook 个性,能够让开发者在不编写 类(class)的状况下应用 state 以及其余的 React 个性。并且,如果你应用 React Native 进行挪动利用开发,那么 React Native 从 0.59 版本开始反对 Hook。
并且,应用 Hook 后,咱们能够抽取状态逻辑,使组件变得可测试、可重用,而开发者能够在不扭转组件层次结构的状况下,去重用状态逻辑,更好的实现状态和逻辑拆散的目标。上面是应用 State Hook 的例子。
import React, {useState} from "react";
const StateHook = () => {const [count, setCount] = useState(0);
return (
<div>
<p>you clicked {count} times</p>
<button type="button" onClick={() => setCount(count + 1)}>
click me
</button>
</div>
);
};
在下面的示例红,useState 就是一个 Hook,即通过在函数组件里调用它来给组件增加一些外部 State,React 会在反复渲染时保留这个 State。useState 会返回一对值:以后状态和一个让你更新它的函数,你能够在事件处理函数中或其余一些中央调用这个函数。它相似 class 组件的 this.setState,然而它不会把新的 state 和旧的 state 进行合并。(咱们会在应用 State Hook 里展现一个比照 useState 和 this.state 的例子)。
二、Hook 基本概念
Hook 为函数式组件提供了状态,它反对在函数组件中进行数据获取、订阅事件解绑事件等等,学习 React Hook 之前,咱们咱们先了解以下一些根底概念。
2.1 useState
useState 让函数组件具备了状态的能力。例如,后面用到的计数器示例就用到了 useState。
function App () {const [count, setCount] = useState(0)
return (
<div>
点击次数: {count}
<button onClick={() => { setCount(count + 1)}}> 点我 </button>
</div>
)
}
能够发现,useState 应用上非常简单,第一个值是咱们的 state,第二个值是一个函数,用来批改该 state 的值。useState 反对指定 state 的默认值,比方 useState(0), useState({a: 1}),除此之外,useState 还反对咱们传入一个通过逻辑计算出默认值,比方。
function App (props) {const [ count, setCount] = useState(() => {return props.count || 0})
return (...)
}
2.2 useEffect
Effect Hook 能够让你处理函数组件中的副作用。在 React 中,数据获取、设置订阅、手动的更改 DOM 都能够称为副作用,能够将副作用分为两种,一种是须要清理的,另外一种是不须要清理的。比方网络申请、DOM 更改、日志这些副作用都不要清理。而比方定时器,事件监听则是须要解决的,而 useEffect 让开发者能够解决这些副作用。
上面是应用 useEffect 更改 document.title 题目的示例,代码如下。
import React, {useState,useEffect} from "react";
function App () {const [ count, setCount] = useState(0)
useEffect(() => {document.title = count})
return (
<div>
以后页面 ID: {count}
<button onClick={() => { setCount(count + 1)}}> 点我 </button>
</div>
)
}
export default App;
如果你相熟 React 类组件的生命周期函数,那么咱们能够把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
在类组件中,咱们绑定事件、解绑事件、设定定时器、查找 Dom 都须要通过 componentDidMount、componentDidUpdate、componentWillUnmount 生命周期来实现,而 useEffect 的作用就相当于这三个生命周期函数,只不过须要通过传参来决定是否调用它。useEffect 会返回一个回调函数,作用于革除上一次副作用遗留下来的状态,如果该 useEffect 只调用一次,该回调函数相当于 componentWillUnmount 生命周期。
例如有上面一个 useEffect 综合的例子,代码如下。
import React, {useState,useEffect} from "react";
function App () {const [ count, setCount] = useState(0)
const [width, setWidth] = useState(document.body.clientWidth)
const onChange = () => {setWidth(document.body.clientWidth)
}
useEffect(() => {
// 相当于 componentDidMount
window.addEventListener('resize', onChange, false)
return () => {
// 相当于 componentWillUnmount
window.removeEventListener('resize', onChange, false)
}
}, [])
useEffect(() => {
// 相当于 componentDidUpdate
document.title = count;
})
useEffect(() => {console.log(`count change: count is ${count}`)
}, [count])
return (
<div>
页面名称: {count}
页面宽度: {width}
<button onClick={() => { setCount(count + 1)}}> 点我 </button>
</div>
)
}
export default App;
在下面例子中,咱们须要解决两种副作用,即既要解决 title,还要监听屏幕宽度的扭转,依照 类组件的写法咱们须要在生命周期中解决这些逻辑,不过在 Hooks 中,咱们只须要应用 useEffect 就能解决这些问题。
后面说过,useEffect 就是用来解决副作用的,而革除上一次留下的状态就是它的作用之一。因为 useEffect 是每次 render 之后就会被调用,此时 title 的扭转就相当于 componentDidUpdate,但咱们不心愿事件监听每次 render 之后进行一次绑定和解绑,此时就用到了 useEffect 函数的第二个参数。
那什么时候会用到 useEffect 的第二个参数呢?次要有以下场景:
- 组件每次执行 render 之后 useEffect 都会调用,此时相当于执行类组件的 componentDidMount 和 componentDidUpdate 生命周期。
- 传入一个空数组 [], 此时 useEffect 只会调用一次,相当于执行类组件的 componentDidMount 和 componentWillUnmount 生命周期。
- 传入一个数组,其中包含变量,只有这些变量变动时,useEffect 才会执行。
2.3 useMemo
在传统的函数组件中,当在一个父组件中调用一个子组件的时候,因为父组件的 state 产生扭转会导致父组件更新,而子组件尽管没有产生扭转然而也会进行更新,而 useMemo 就是函数组件为了避免这种不必要的更新而采取的伎俩,其作用相似于类组件的 PureComponent。
那 useMemo 是如何应用的呢,看上面的一个例子。
function App () {const [ count, setCount] = useState(0)
const add = useMemo(() => {return count + 1}, [count])
return (
<div>
点击次数: {count}
<br/>
次数加一: {add}
<button onClick={() => { setCount(count + 1)}}> 点我 </button>
</div>
)
}
须要留神的是,useMemo 会在渲染的时候执行,而不是渲染之后执行,这一点和 useEffect 有区别,所以 useMemo 不倡议办法中有副作用相干的逻辑。
2.4 useCallback
useCallback 是 useMemo 的语法糖,基本上能用 useCallback 实现的都能够应用 useMemo,不过 useCallback 也有本人的应用场景。比方,在 React 中咱们常常会面临子组件渲染优化的问题,尤其在向子组件传递函数 props 时,每次的渲染 都会创立新函数,导致子组件不必要的渲染。而 useCallback 应用的是缓存的函数,这样把这个缓存函数作为 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}/>
<button onClick={() => setCount(count + 1)}> 点我 +1</button>
</div>;
}
function Child({callback}) {const [count, setCount] = useState(() => callback());
useEffect(() => {setCount(callback());
}, [callback]);
return <div>
子组件:{count}
</div>
}
export default Parent;
须要阐明的是,React.memo 和 React.useCallback 肯定记得配对应用,缺了一个都可能导致性能不升反“降”,毕竟无意义的浅比拟也会耗费一些性能。
2.5 useRef
在 React 中,咱们应用 Ref 来获取组件的实例或者 DOM 元素,咱们能够应用两种形式来创立 Ref:createRef 和 useRef,如下所示。
import React, {useState, useRef} from 'react'
function App(){const [count, setCount] = useState(0)
const counterEl = useRef(null)
const increment = () => {setCount(count + 1)
console.log(counterEl)
}
return (
<>
Count: <span ref={counterEl}>{count}</span>
<button onClick={increment}> 点我 +</button>
</>
)
}
2.6 useReducer
useReducer 的作用相似 redux 中的性能,相较于 useState,useReducer 适宜一些逻辑较简单且蕴含多个子值的状况。reducer 承受两个参数,第一个参数是一个 reducer,第二个参数是初始 state,返回值为最新的 state 和 dispatch 函数。
依照官网的说法,useReducer 适宜用于简单的 state 操作逻辑,嵌套的 state 的对象的场景。上面是官网给出的示例。
import React, {useReducer} from 'react';
function Reducers () {const initialState={count:0}
const [count,dispatch] = useReducer((state,avtion) => {switch(avtion.type) {
case 'add':
return state+1;
case 'minus':
return state-1
default:
return state
}
},0)
return (
<div>
<div>{count}</div>
<button onClick={() => {dispatch({type: 'add'})}}> 加 </button>
<button onClick={() => {dispatch({type: 'minus'})}}> 减 </button>
</div>
)
}
export default Reducers
2.7 useImperativeHandle
useImperativeHandle 能够让开发者在应用 ref 时自定义裸露给父组件的实例值。其意思就是,子组件能够选择性的裸露一些办法给父组件,而后暗藏一些公有办法和属性,官网倡议,useImperativeHandle 最好与 forwardRef 一起应用。
import React, {useRef, forwardRef, useImperativeHandle} from 'react'
const App = forwardRef((props,ref) => {const inputRef = useRef()
useImperativeHandle(ref,()=>({focus : () =>{inputRef.current.focus()
}
}),[inputRef])
return <input type="text" ref={inputRef}/>
})
export default function Father() {const inputRef = useRef()
return (
<div>
<App ref={inputRef}/>
<button onClick={e=>inputRef.current.focus()}> 获取焦点 </button>
</div>
)
}
在示例中,咱们通过 useImperativeHandle 将子组件的实例属性输入到父组件,而子组件外部通过 ref 更改 current 对象后组件不会从新渲染,须要扭转 useState 设置的状态能力更改。
除了下面介绍的几种 Hook API 之外,React Hook 常见的 API 还包含 useLayoutEffect、useDebugValue。
自定义 Hook
应用 Hook 技术,React 函数组件的 this 指向、生命周期逻辑冗余的问题都已失去解决,不过 React 开发中另一个比拟常见的问题,逻辑代码复用依然没有失去解决。如果要解决这个问题,须要通过自定义 Hook。
所谓的自定义 Hook,其实就是指函数名以 use 结尾并调用其余 Hook 的函数,自定义 Hook 的每个状态都是齐全独立的。例如,上面是应用自定义 Hook 封装 axios 实现网络申请的示例,代码如下。
import axios from 'axios'
import {useEffect, useState} from 'react';
const useAxios = (url, dependencies) => {const [isLoading, setIsLoading] = useState(false);
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {setIsLoading(true);
axios.get(url).then((res) => {setIsLoading(false);
setResponse(res);
}).catch((err) => {setIsLoading(false);
setError(err);
});
}, dependencies);
return [isLoading, response, error];
}
export default useAxios;
在下面的代码中,咱们应用 React 已有的 API 实现自定义 Hook 的性能。而具体应用时,自定义 Hook 的应用办法和 React 官网提供的 Hook API 应用上相似,如下所示。
function App() {
let url = 'http://api.douban.com/v2/movie/in_theaters';
const [isLoading, response, error] = useAxios(url, []);
return (
<div>
{isLoading ? <div>loading...</div> :
(error ? <div> There is an error happened </div> : <div> Success, {response} </div>)}
</div>
)
}
export default App;
能够发现,相比于函数属性和高阶组件等形式,自定义 Hook 则更加的简洁易读,不仅于此,自定义 Hook 也不会引起之组件嵌套天堂问题。
尽管 React 的 Hooks 有着诸多的劣势。不过,在应用 Hooks 的过程中,须要留神以下两点:
- 不要在循环、条件或嵌套函数中应用 Hook,并且只能在 React 函数的顶层应用 Hook。之所以要这么做,是因为 React 须要利用调用程序来正确更新相应的状态,以及调用相应的生命周期函数函数。一旦在循环或条件分支语句中调用 Hook,就容易导致调用程序的不一致性,从而产生难以预料到的结果。
- 只能在 React 函数式组件或自定义 Hook 中应用 Hook。
同时,为了防止在开发中造成一些低级的谬误,能够装置一个 eslint 插件,命令如下。
yarn add eslint-plugin-react-hooks --dev
而后,在 eslint 的配置文件中增加如下一些配置。
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}