React Hook快速入门

4,055 阅读8分钟

Hook简介

React Hooks是React 16.7.0-alpha版本推出的新特性,目的是解决React的状态共享问题。称之为状态共享可能描述的并不是很恰当,称为状态逻辑复用可能会更恰当,因为React Hooks只共享数据处理逻辑,并不会共享数据本身。 在React应用开发中,状态管理是组件开发必不可少的内容。以前,为了对状态进行管理,最通常的做法是使用类组件或者直接使用redux等状态管理框架。例如:

class Hook extends React.Component {
  
constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
   return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

现在,开发者可以直接使用React Hooks提供的State Hook来处理状态,针对那些已经存在的类组件,也可以使用State Hook很好地进行重构。例如:

import React, { useState } from 'react';

function Hook() {
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

从上面的示例可以发现,Example变成了一个函数组件,此函数组件有自己的状态,并且还可以更新自己的状态。之所以可以如此操作,是因为使用了useState,useState是react自带的一个hook函数,它的作用就是用来声明状态变量。

Hook API

一直以来,React就在致力于解决一个问题,即状态组件的复用问题。在React应用开发中,我们都是通过组件和自上而下传递的数据流来将大型的视图拆分成独立的可复用组件。但是在实际项目开发中,复用一个带有业务逻辑的组件依然是一个难题。 众所周知,React提供了两种创建组件的方式,即函数组件和类组件。函数组件是一个普通的JavaScript函数,接受props对象并返回React元素,函数组件更符合React数据驱动视图的开发思想。不过,函数组件一直以来都因为缺乏类组件诸如状态、生命周期等种种特性,也因为这些原因函数组件得不到开发者的青睐,而Hooks的出现就是让函数式组件拥有类组件的特性。 为了让函数组件拥有类组件的诸如状态、生命周期等特性,React 提供了3个核心的api,即State Hooks、Effect Hooks和Custom Hooks。 useState就是React提供最基础、最常用的Hook,主要用来定义和管理本地状态。例如,下面是使用useState实现一个最简单的计数器,代码如下:

import React, { useState } from 'react'

function App() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <button onClick={()=> setCount(count + 1)}>+</button>
            <span>{count}</span>
            <button onClick={() => setCount((count) => count - 1)}>-</button>
        </div>
    );
}

export default App;

在上面的示例中,使用useState来定义一个状态,与类组件的状态不同,函数组件的状态可以是对象也可以是基础类型值。useState返回的是一个数组,数组的第一个对象表示当前状态的值,第二个对象表示用于更改状态的函数,类似于类组件的setState。 函数组件中如果存在多个状态,既可以通过一个useState声明对象类型的状态,也可以通过useState多次声明状态。例如:

//声明对象类型状态
const [count, setCount] = useState({
    count1: 0,
    count2: 0
});
 
//多次声明
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);

可以发现,相比于声明对象类型状态,多次声明状态的方式更加方便,主要是因为更新函数是采用的替换的方式而不是合并的方式。 不过,如果要在函数组件中处理多层嵌套数据逻辑时,使用useState就显得力不从心了。值得庆幸的是,开发者可以使用React提供的useReducer来处理此类问题。例如:

import React, {useReducer} from 'react'

const reducer = function (state, action) {
    switch (action.type) {
        case "increment":
            return {count: state.count + 1};
        case "decrement":
            return {count: state.count - 1};
        default:
            return {count: state.count}
    }
};

function Example() {
    const [state, dispatch] = useReducer(reducer, {count: 0});
    const {count} = state;
    return (
        <div>
            <button onClick={() => dispatch({type: "increment"})}>+</button>
            <span>{count}</span>
            <button onClick={() => dispatch({type: "decrement"})}>-</button>
        </div>
    );
}

export default Example;

可以发现,useReducer接受reducer函数和默认值两个参数,并返回当前状态state和dispatch函数的数组,使用方式与Redux基本一致,不同之处在于,Redux的默认值是通过给reducer函数设置默认参数的方式给定的。 useReducer之所以没有采用Redux的方式设置默认值,是因为React认为状态的的默认值可能是来自于函数组件的props。例如:

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, { count: initialState });
   ...
}

解决了函数组件中内部状态的问题,接下来亟待解决的就是函数组件中生命周期函数的问题。在React的函数式编程思想中,生命周期函数是沟通函数式和命令式的桥梁,根据生命周期函数开发者可以执行相关的操作,如网络请求和操作DOM。

import React, {useState, useEffect} from 'react';

function Example() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        console.log('componentDidMount...')
console.log('componentDidUpdate...')
        return () => {
            console.log('componentWillUnmount...')
        }
    });

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

export default Example;

执行上面的代码,然后点击按钮执行加法操作时,生命周期函数调用情况下图所示。

在这里插入图片描述
可以看到,每次执行组件更新时useEffect中的回调函数都会被调用。并且在点击按钮执行更新操作时还会触发componentWillUnmount生命周期,之所以在重新绘制前执行销毁操作,是为了避免造成内存泄露。 因此可以将useEffect视为componentDidMount、componentDidUpdate和componentWillUnmount的组合,并用它关联函数组件的生命周期。 需要说明是,类组件的componentDidMount或componentDidUpdate生命周期函数都是在DOM更新后同步执行的,但useEffect并不会在DOM更新后同步执行。因此,使用useEffect的并不会阻塞浏览器更新界面。如果需要模拟生命周期的同步执行,可以使用React提供的useLayoutEffect Hook。

自定义Hook

众所周知,要在类组件之间共享一些状态逻辑是非常麻烦的,常规的做法都是通过高阶组件或者是函数的属性来解决。不过,新版的React允许开发者创建自定义Hook来封装共享状态逻辑,且不需要向组件树中增加新的组件。 所谓的自定义Hook,其实就是指函数名以use开头并调用其他Hook的函数,自定义Hook的每个状态都是完全独立的。例如,下面是使用axios实现网络请求的示例,代码如下:

import axios from 'axios'

export 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];
};

如上所示,就是使用axios开发请求数据的自定义Hook。使用方法和系统提供的Hook类似,直接调用即可。例如:

function Example() {
    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 Example;

可以发现,相比于函数属性和高阶组件等方式,自定义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"
  }
}

借助于React提供的Hooks API,函数组件可以实现绝大部分的类组件功能,并且Hooks在共享状态逻辑、提高组件复用性上也具有一定的优势。可以预见的是,Hooks将是React未来发展的重要方向。