关于React Hooks,你不得不知的事

4,788 阅读7分钟

React Hooks是React 16.8发布以来最吸引人的特性之一。在开始介绍React Hooks之前,让咱们先来理解一下什么是hooks。wikipedia是这样给hook下定义的:

In computer programming, the term hooking covers a range of techniques used to alter or augment the behaviour of an operating system, of applications, or of other software components by intercepting function calls or messages or events passed between software components. Code that handles such intercepted function calls, events or messages is called a hook.

通俗来说,Hook(钩子)就是通过拦截软件和系统内部函数调用和消息通信来增强原有功能的技术。而React Hooks想要增强哪些功能呢?设想你的项目中已经有一大堆组件,这些组件各自都拥有自己的状态。那么一旦你想重用某些特定的带状态逻辑,就得大幅度重构你的应用。现在有了React Hooks,你只需要抽离这些带状态的逻辑代码,然后它们可以更好地进行重用, 而且独立出来的代码也更容易进行测试和管理。有了React Hooks后,你可以在函数式组件中实现之前在带状态组件中能做到的任何事,你能够更灵活地实现你的应用代码。

接下来,让我们看看React Hooks在实际项目中到底怎么使用。

状态管理

对于业务性组件来说,状态管理肯定是不可避免的。以前,我们通常写Class组件来管理业务逻辑,或者使用redux来全局管理状态。现在我们可以利用React Hooks新提供的State Hook来处理状态,针对那些已经写好的Class组件,我们也可以利用State Hook很好地进行重构, 先来看下面这段代码:

import React from 'react';
class Person extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
          username: "scq000"
      };
  }
  
  render() {
      return (
        <div>
            <p>Welcome to homepage. {state.username}</p>
            <input type="text" placeholder="input a username" onChange={(event) => this.setState({ username: event.target.value)})}></input>
        </div>
      );
  }
}

接下来尝试将它重构成函数式组件:

import React, {useState} from 'react';

export const Person = () => {
  const [state, setState] = useState({username: "scq000"});
  
  return (
  	<div>
  		<p>Welcome to homepage. {state.username}</p>
		<input type="text" placeholder="input a username" onChange={(event) => setState({username: event.target.value})}></input>
  	</div>
  )
}

如上面这段代码,我们首先使用useState api 来声明一个内部状态,接着声明一个新的状态变量state,以及它的setter方法。在这里,为了减少重构的工作量我特意选择了state这个变量名,你也可以单独将每个独立的状态提取出来使用, 比如使用代码const [username, setUsername] = userState("scq000")。在随后的组件内部,我们就可以利用这个内部状态来处理业务逻辑了。由于是函数式组件的写法,我们也能够避免很多this绑定,而且这部分逻辑在后续使用过程中也可以抽离出来进行重用。不过这里有个需要注意的点是:当你使用set方法的时候,旧状态不会自动merge到新状态中去,所以你如果提取的状态是个对象,且有多个属性时,需要使用如下语法进行状态的更新:

setState({
    ...state,
  	username: event.target.value
});

生命周期管理

我们都知道,组件的生命周期管理是整个react组件的灵魂所在。利用生命周期函数,我们可以控制整个组件的加载、更新和卸载。React Hooks中提供了Effect钩子,使我们可以在函数式组件中实现这些功能。

为了便于理解,接下来我将分别演示如何利用Effect钩子实现原本在Class组件中的各个生命周期方法。下面这段代码是我们熟悉的Class组件:

import React from 'react';
class Person extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
          username: "scq000"
      };
  }
  
  componentDidMount() {
      console.log('componentDidMount: 组件加载后')
  }
  
  componentWillUnmount() {
      console.log('componentWillUnmount: 组件卸载, 做一些清理工作')
  }
  
  componentDidUpdate(prevProps, prevState) {
      if(prevState.username !== this.state.username) {
          console.log('componentDidUpdate: 更新usernmae')
      }
  }
  
  render() {
      return (
        <div>
            <p>Welcome to homepage. {state.username}</p>
            <input type="text" placeholder="input a username" onChange={(event) => this.setState({ username: event.target.value)})}></input>
        </div>
      );
  }
}

现在我们利用Effect重构一下:

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

export const Person = () => {
  const [state, setState] = useState({username: "scq000"});
  
  useEffect(() => {
      console.log('componentDidMount: 组件加载后')
      return () => {
      	console.log('componentWillUnmount: 组件卸载, 做一些清理工作')
      }
  }, []);
  
  useEffect(() => {
      console.log('componentDidUpdate: 更新usernmae')
  }, [state.username]);
  
  return (
  	<div>
  		<p>Welcome to homepage. {state.username}</p>
		<input type="text" placeholder="input a username" onChange={(event) => setState({username: event.target.value})}></input>
  	</div>
  )
}

可以看到,我们利用副作用钩子很好地实现了原本的生命周期方法。通常我们会利用组件的生命周期函数去获取数据,操作DOM等,而这些操作都被称作副作用(side effect)。这些副作用逻辑一般都比较复杂,也是bug频发的地段。 所以我们可以针对每一段逻辑单独使用一个Effect钩子,便于后期维护和调试。

在使用过程中,useEffect方法需要传入两个参数,第一个参数是回调函数:这个回调函数会在每次组件渲染后执行,包括初始化渲染以及每次更新时。另一个参数,则是状态依赖项(数组形式),一旦检测到依赖项数据变动,组件会更新,并且回调函数都会被再次执行一遍,从而实现componentDidUpdate的功能。如果你传入一个空依赖,就能实现原来componentDidMount的效果,即只会执行一次。回调函数中如果返回的是闭包,这个返回的闭包函数将会在组件重新渲染前执行,所以你可以在这个位置做一些清理操作,从而实现componentWillUnmount的功能。

还有要注意的是componentWillMountcomponentWillUpdate两个生命周期方法在新版本的React中已经不推荐使用了,具体原因可以查看这里

至此,我们就学会如何利用Effect钩子在函数式组件中实现所有生命周期方法,从而管理我们的应用了。

自定义Hook

重用和抽象一直都是编程中要解决的问题。我们可以自己封装想要的Hook, 从而实现代码逻辑的重用和抽象。

封装自定义hook其实很简单,就是包装一个自定义函数,然后根据功能将其状态和对应的effect逻辑封装进去:

export const useFetch = (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];
}

这里我们简单地封装了一个请求数据的Hook,使用方法跟其他Hook类似,直接调用就可以了:

export const Person = () => {
  const [isLoading, response, error] = useFetch("http://example.com/getPersonInfo", []); 
  
  return (
  	<div>
  		{isLoading ? 
  			<div>loading...</div>
  			:
  			(
  				error ? <div> There is an error happened. {error.message} </div>
  					  : <div> Welcome, {response.userName} </div>
  			)
  		}
  	</div>
  )
}

注意事项

在使用Hooks的过程中,需要注意的两点是:

  • 不要在循环,条件或嵌套函数中调用Hook,必须始终在React函数的顶层使用Hook。这是因为React需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。

  • 只能在React函数式组件或自定义Hook中使用Hook。

为了避免我们无意中破坏这些规则,你可以安装一个eslint插件:

npm install eslint-plugin-react-hooks --save-dev

并在配置文件中使用它:

{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error"
  }
}

这样,一旦你违法上述这些原则,就会获得相应的提示。

总结

本文介绍了React Hook的使用方式,并通过几个简单的例子演示了如何在函数式组件中进行状态管理和生命周期管理。官方目前提供了很多基础的Hook,如useContext, useReducer, useMemo等,大家可以酌情在项目中使用。

参考资料

https://reactjs.org/docs/hooks-reference.html

——本文首发于个人公众号,转载请注明出处———

微信扫描二维码,关注我的公众号
最后,欢迎大家关注我的公众号,一起学习交流。