React Hook 快速上手
一、 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"
}
}