ReactHook实战
一、Hook简介1.1Hook的历史在ReactHook出现之前的版本中,组件主要分为功能组件和类组件两种。其中,功能组件通常被认为只负责UI的渲染,没有自己的状态或业务逻辑代码,是一个纯功能。类组件不同。类组件有自己的内部状态。界面的显示结果通常是由props和state决定的,不再那么纯粹了。功能组件和类组件有以下缺点:状态逻辑难以复用。在类组件中,为了复用一些状态逻辑,社区提出了renderprops或者hoc等解决方案,但是这些方案对组件的侵入性太大,组件嵌套容易造成嵌套地狱的问题。滥用组件状态。大部分开发者在编写组件时,无论组件是否有内部状态,是否会执行生命周期函数,都会将组件写成类组件,造成不必要的性能开销。附加任务处理。在使用类组件开发应用程序时,开发者需要格外注意this、添加和移除事件监听器等问题。目前,在功能组件流行的时候,类组件正在逐渐被淘汰。然而,功能组件并非没有缺点。以前的写法,管理功能组件的状态共享比较麻烦。例如下面的函数组件是一个纯函数,它的输出只由参数props决定,不受其他任何因素的影响。functionApp(props){const{name,age}=props.inforeturn(你好,我是({name}),我是({age})old
)}在上面的函数式组件中,一旦我们需要给组件添加状态,我们只能将组件重写为类组件,因为函数式组件没有实例,没有生命周期。所以我们说Hook之前函数组件和类组件最大的区别其实就是状态的有无。1.2Hook概述为了解决函数式组件状态的问题,React在16.8版本中加入了Hook特性,开发者可以在不编写类的情况下使用状态等React特性。并且,如果你使用ReactNative进行移动应用程序开发,那么ReactNative从0.59版本开始支持Hooks。而且,在使用Hook之后,我们可以将状态逻辑抽取出来,让组件可测试、可复用,开发者可以在不改变组件层次结构的情况下复用状态逻辑,从而更好地实现状态与逻辑的分离。目的。下面是一个使用StateHook的例子。从“反应”中导入反应,{useState};constStateHook=()=>{const[count,setCount]=useState(0);return(
你点击了{count}次
setCount(count+1)}>点击我);};在上面的例子中,useState是一个Hook,即通过函数在组件中调用它来为组件添加一些内部状态,React在重新渲染时会保持这个状态。useState返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理程序或其他地方调用它。类似于类组件的this.setState,但不会合并新旧状态。(我们将在使用StateHook中展示一个比较useState和this.state的例子)。2、Hook的基本概念Hook为功能组件提供状态。支持功能组件中的数据获取,订阅事件,解绑事件等。在学习ReactHook之前,我们先了解以下几个基本概念。2.1useStateuseState允许功能组件具有状态能力。比如前面用到的计数器例子就用到了useState。functionApp(){const[count,setCount]=useState(0)return(
点击次数:{count}{setCount(count+1)}}>clickme
)}可以发现useState使用起来非常简单。第一个值是我们的状态,第二个值是修改状态值的函数。useState支持指定state的默认值,比如useState(0),useState({a:1}),另外useState也支持我们传入一个逻辑计算出来的默认值,比如。functionApp(props){const[count,setCount]=useState(()=>{returnprops.count||0})return(...)}2.2useEffectEffectHook允许你处理函数组件中的副作用。在React中,获取数据、设置订阅、手动更改DOM都可以称为副作用。副作用分为两种,一种是需要清理的,一种是不需要清理的。不应清除网络请求、DOM更改和日志等副作用。例如需要处理定时器和事件监听,useEffect允许开发者处理这些副作用。下面是使用useEffect改变document.title的标题的例子,代码如下。importReact,{useState,useEffect}from"react";functionApp(){const[count,setCount]=useState(0)useEffect(()=>{document.title=count})return(
当前页面ID:{count}{setCount(count+1)}}>点我
)}exportdefaultApp;如果你熟悉React类组件的生命周期功能,那么我们可以把useEffectHook看作是三个函数的组合:componentDidMount、componentDidUpdate和componentWillUnmount。在类组件中,我们需要通过componentDidMount、componentDidUpdate、componentWillUnmount的生命周期绑定事件、解除绑定事件、设置定时器、查找Dom,而useEffect的作用就相当于这三个生命周期函数。您需要传递参数来决定是否调用它。useEffect将返回一个回调函数来清除最后一个副作用遗留下来的状态。如果只调用一次useEffect,回调函数相当于componentWillUnmount生命周期。比如下面有一个useEffect合成的例子,代码如下。importReact,{useState,useEffect}from"react";functionApp(){const[count,setCount]=useState(0)const[width,setWidth]=useState(document.body.clientWidth)constonChange=()=>{setWidth(document.body.clientWidth)}useEffect(()=>{//相当于componentDidMountwindow.addEventListener('resize',onChange,false)return()=>{//相当于componentWillUnmountwindow.removeEventListener('resize',onChange,false)}},[])useEffect(()=>{//相当于componentDidUpdatedocument.title=count;})useEffect(()=>{console.log(`countchange:countis${count}`)},[count])return(
pagename:{count}pagewidth:{width}{setCount(count+1)}}>点我
)}exportdefaultApp;在上面的例子中,我们需要处理两个副作用,即处理标题和监听屏幕宽度的变化,按下按照类组件的写法,我们需要在生命周期中处理这些逻辑,但是在Hooks中,我们只需要使用useEffect就可以解决这些问题。前面说了useEffect是用来处理副作用的,清除上次留下的状态就是它的作用之一。由于每次render后都会调用useEffect,此时title的变化相当于componentDidUpdate,但是我们不希望事件监听器在每次render后绑定和解除绑定,所以使用了useEffect的第二个函数参数。那么useEffect的第二个参数什么时候用呢?主要有以下几种场景:useEffect会在每次组件渲染后调用,相当于执行类组件的componentDidMount和componentDidUpdate生命周期。传入一个空数组[],此时useEffect只会被调用一次,相当于执行类组件的componentDidMount和componentWillUnmount生命周期。传入一个包含变量的数组,useEffect只有在这些变量发生变化时才会执行。2.3useMemo在传统的函数组件中,在父组件中调用子组件时,父组件会因为父组件状态的变化而更新,子组件即使没有变化也会更新,而useMemo是函数组件采用的方法来防止这种不必要的更新,其作用类似于类组件的PureComponent。那么useMemo是如何使用的,看下面的例子。functionApp(){const[count,setCount]=useState(0)constadd=useMemo(()=>{returncount+1},[count])return(
点击次数:{count}
增加1次:{add}{setCount(count+1)}}>clickme
)}需要注意的是useMemo渲染Execute时会显示,渲染后不会显示,这个和useEffect不同,所以useMemo不建议方法中有副作用相关的逻辑。2.4useCallbackUseCallback是useMemo的语法糖。基本上,useMemo可以用于任何可以用useCallback实现的东西,但是useCallback也有它自己的使用场景。比如在React中,我们经常会面临子组件渲染优化的问题,尤其是给子组件传递函数props时,每次渲染都会创建一个新的函数,导致子组件出现不必要的渲染。而useCallback使用了一个缓存函数,所以将这个缓存函数作为props传递给子组件可以减少不必要的渲染。importReact,{useState,useCallback,useEffect}from'react';functionParent(){const[count,setCount]=useState(1);const[val,setVal]=useState('');constcallback=useCallback(()=>{returncount;},[count]);return
父组件:{count}
setCount(count+1)}>clickme+1;}functionChild({callback}){const[count,setCount]=useState(()=>callback());useEffect(()=>{setCount(回调());},[回调]);return
Subcomponent:{count}
}exportdefaultParent;需要注意的是React.memo和React.useCallback一定要成对使用。如果少了一个,性能可能不增反“减”。毕竟,无意义的浅层比较也会消耗一些性能。2.5useRef在React中,我们使用Ref来获取组件实例或者DOM元素。我们可以使用两种方式来创建Ref:createRef和useRef,如下所示。importReact,{useState,useRef}from'react'functionApp(){const[count,setCount]=useState(0)constcounterEl=useRef(null)constincrement=()=>{setCount(count+1)控制台.log(counterEl)}return(<>Count:
{count}clickme+>)}2.6useReduceruseReducer函数类似于redux中的函数。与useState相比,useReducer适用于逻辑比较复杂,包含多个子值的情况。reducer接受两个参数,第一个参数是一个reducer,第二个参数是初始状态,返回值是最新的状态和dispatchfunction。按照官方的说法,useReducer适用于复杂的状态操作逻辑和嵌套状态对象场景。下面是一个官方的例子。importReact,{useReducer}from'react';functionReducers(){constinitialState={count:0}const[count,dispatch]=useReducer((state,avtion)=>{switch(avtion.type){case'add':返回状态+1;case'minus':返回状态-1默认:返回状态}},0)return({count}
{dispatch({type:'add'})}}>plus{dispatch({type:'minus'})}}>minus )}exportdefaultReducers2.7useImperativeHandleuseImperativeHandle允许开发者自定义在使用ref时暴露给父组件的实例值。就是说子组件可以有选择地把一些方法暴露给父组件,然后隐藏一些私有的方法和属性。官方建议useImperativeHandle最好和forwardRef一起使用。importReact,{useRef,forwardRef,useImperativeHandle}from'react'constApp=forwardRef((props,ref)=>{constinputRef=useRef()useImperativeHandle(ref,()=>({focus:()=>{inputRef.current.focus()}}),[inputRef])return})exportdefaultfunctionFather(){constinputRef=useRef()返回(inputRef.current.focus()}>Getfocus )}在示例中,我们将useImperativeHandle传递给childcomponent属性输出到父组件,子组件内部通过ref改变当前对象后组件不会重新渲染,需要改变useState设置的state才能改变。除了上面介绍的几种HookAPI外,ReactHook的常用API还有useLayoutEffect和useDebugValue。CustomHook使用了Hook技术,解决了React函数组件的this点,以及生命周期逻辑冗余的问题,但是React开发中另一个常见的问题,逻辑代码复用,还没有解决。如果要解决这个问题,就需要自定义Hook。所谓自定义Hook,其实就是指函数名以use开头的函数,调用其他的Hook函数。自定义Hook的每个状态都是完全独立的。比如下面是一个使用自定义Hook封装axios实现网络请求的例子。代码如下。从'axios'导入axios从'react'导入{useEffect,useState};constuseAxios=(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);返回[isLoading,response,error];}exportdefaultuseAxios;在上面的代码中,我们利用React已有的API实现了自定义Hook的功能。在具体使用上,自定义Hook的用法与React官方提供的HookAPI类似,如下所示。functionApp(){leturl='http://api.douban.com/v2/movie/in_theaters';const[isLoading,response,error]=useAxios(url,[]);return({isLoading?
loading...
:(error?
发生错误
:
Success,{response}
)}
)}导出默认应用;可以发现,相比于函数属性和高阶组件,自定义Hooks更加简洁易读。不仅如此,自定义Hooks不会造成组件嵌套地狱问题。虽然React的Hooks有很多优点。但是,在使用Hooks的过程中,需要注意以下两点:不要在循环、条件、嵌套函数中使用Hooks,只在React函数的顶层使用Hooks。这样做的原因是React需要使用调用序列来正确更新相应的状态并调用相应的生命周期功能函数。Hook一旦在循环或条件分支语句中被调用,很容易造成调用顺序不一致,造成不可预知的后果。Hooks只能用在React函数式组件或自定义Hooks中。同时,为了避免开发中出现一些低级错误,可以安装一个eslint插件,命令如下。yarnaddeslint-plugin-react-hooks--dev然后在eslint配置文件中添加如下配置。{"plugins":[//..."react-hooks"],"rules":{//..."react-hooks/rules-of-hooks":"error","react-hooks/exhaustive-deps":"警告"}}