React Hooks 快速上手

React Hook 快速上手

一、 Hook 简介

1.1 Hook历史

在React Hook出现之前的版本中,组件主要分为两种:函数式组件和类组件。其中,函数式组件通常只考虑负责UI的渲染,没有自身的状态也没有业务逻辑代码,是一个纯函数。而类组件则不同,类组件有自己的内部状态,界面的显示结果通常由props 和 state 决定,因此它也不再那么纯洁。函数式组件,类组件有如下一些缺点:

  • 状态逻辑难以复用。在类组件中,为了重用某些状态逻辑,社区提出了render props 或者 hoc 等方案,但是这些方案对组件的侵入性太强,并且组件嵌套还容易造成嵌套地狱的问题。
  • 滥用组件状态。大多数开发者在编写组件时,不管这个组件有木有内部状态,会不会执行生命周期函数,都会将组件编写成类组件,这造成不必要的性能开销。
  • 额外的任务处理。使用类组件开发应用时,需要开发者额外去关注 this 、事件监听器的添加和移除等等问题。

在函数式组件大行其道的当前,类组件正在逐渐被淘汰。不过,函数式组件也并非毫无缺点,在之前的写法中,想要管理函数式组件状态共享就是比较麻烦的问题。例如,下面这个函数组件就是一个纯函数,它的输出只由参数props决定,不受其他任何因素影响。

1function App(props) { 2 const {name, age } = props.info 3 return ( 4 <div style={{ height: '100%' }}> 5 <h1>Hello,i am ({name}),and i am ({age}) old</h1> 6 </div> 7 ) 8} 9

在上面的函数式组件中,一旦我们需要给组件加状态,那就只能将组件重写为类组件,因为函数组件没有实例,没有生命周期。所以我们说在Hook之前的函数组件和类组件最大的区别其实就是状态的有无。

1.2 Hook 概览

为了解决函数式组件状态的问题,React 在16.8版本新增了Hook特性,可以让开发者在不编写 类(class) 的情况下使用 state 以及其他的 React 特性。并且,如果你使用React Native进行移动应用开发,那么React Native 从 0.59 版本开始支持 Hook。

并且,使用Hook后,我们可以抽取状态逻辑,使组件变得可测试、可重用,而开发者可以在不改变组件层次结构的情况下,去重用状态逻辑,更好的实现状态和逻辑分离的目的。下面是使用State Hook的例子。

1import React, { useState } from "react"; 2 3const StateHook = () => { 4 const [count, setCount] = useState(0); 5 6 return ( 7 <div> 8 <p>you clicked {count} times</p> 9 <button type="button" onClick={() => setCount(count + 1)}> 10 click me 11 </button> 12 </div> 13 ); 14}; 15 16

在上面的示例红,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。

1 function App () { 2 const [count, setCount ] = useState(0) 3 return ( 4 <div> 5 点击次数: { count } 6 <button onClick={() => { setCount(count + 1)}}>点我</button> 7 </div> 8 ) 9 } 10 11

可以发现,useState使用上非常简单,第一个值是我们的 state, 第二个值是一个函数,用来修改该 state的值。useState支持指定 state 的默认值,比如 useState(0), useState({ a: 1 }),除此之外,useState还支持我们传入一个通过逻辑计算出默认值,比如。

1function App (props) { 2 const [ count, setCount ] = useState(() => { 3 return props.count || 0 4 }) 5 return ( 6 ... 7 ) 8 } 9

2.2 useEffect

Effect Hook 可以让你处理函数组件中的副作用。在React中,数据获取、设置订阅、手动的更改 DOM都可以称为副作用,可以将副作用分为两种,一种是需要清理的,另外一种是不需要清理的。比如网络请求、DOM 更改、日志这些副作用都不要清理。而比如定时器,事件监听则是需要处理的,而useEffect让开发者可以处理这些副作用。

下面是使用useEffect更改document.title标题的示例,代码如下。

1import React, { useState,useEffect } from "react"; 2 3function App () { 4 const [ count, setCount ] = useState(0) 5 6 useEffect(() => { 7 document.title = count 8 }) 9 10 return ( 11 <div> 12 当前页面ID: { count } 13 <button onClick={() => { setCount(count + 1 )}}>点我</button> 14 </div> 15 ) 16} 17 18export default App; 19

如果你熟悉React 类组件的生命周期函数,那么我们可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

在类组件中,我们绑定事件、解绑事件、设定定时器、查找 Dom都需要通过 componentDidMount、componentDidUpdate、componentWillUnmount 生命周期来实现,而 useEffect的作用就相当于这三个生命周期函数,只不过需要通过传参来决定是否调用它。useEffect 会返回一个回调函数,作用于清除上一次副作用遗留下来的状态,如果该 useEffect 只调用一次,该回调函数相当于 componentWillUnmount 生命周期。

例如有下面一个useEffect综合的例子,代码如下。

1import React, { useState,useEffect } from "react"; 2 3function App () { 4 const [ count, setCount ] = useState(0) 5 const [ width, setWidth ] = useState(document.body.clientWidth) 6 7 const onChange = () => { 8 setWidth(document.body.clientWidth) 9 } 10 11 useEffect(() => { 12 //相当于 componentDidMount 13 window.addEventListener('resize', onChange, false) 14 return () => { 15 //相当于componentWillUnmount 16 window.removeEventListener('resize', onChange, false) 17 } 18 }, []) 19 20 useEffect(() => { 21 //相当于componentDidUpdate 22 document.title = count; 23 }) 24 25 useEffect(() => { 26 console.log(`count change: count is ${count}`) 27 }, [ count ]) 28 29 return ( 30 <div> 31 页面名称: { count } 32 页面宽度: { width } 33 <button onClick={() => { setCount(count + 1)}}>点我</button> 34 </div> 35 ) 36} 37 38export default App; 39

在上面例子中,我们需要处理两种副作用,即既要处理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 是如何使用的呢,看下面的一个例子。

1function App () { 2 const [ count, setCount ] = useState(0) 3 const add = useMemo(() => { 4 return count + 1 5 }, [count]) 6 return ( 7 <div> 8 点击次数: { count } 9 <br/> 10 次数加一: { add } 11 <button onClick={() => { setCount(count + 1)}}>点我</button> 12 </div> 13 ) 14} 15 16

需要注意的是,useMemo 会在渲染的时候执行,而不是渲染之后执行,这一点和 useEffect 有区别,所以 useMemo不建议方法中有副作用相关的逻辑。

2.4 useCallback

useCallback是useMemo 的语法糖,基本上能用useCallback实现的都可以使用useMemo,不过useCallback也有自己的使用场景。比如,在React 中我们经常会面临子组件渲染优化的问题,尤其在向子组件传递函数props时,每次的渲染 都会创建新函数,导致子组件不必要的渲染。而useCallback使用的是缓存的函数,这样把这个缓存函数作为props传递给子组件时就起到了减少不必要渲染的作用。

1import React, { useState, useCallback, useEffect } from 'react'; 2function Parent() { 3 const [count, setCount] = useState(1); 4 const [val, setVal] = useState(''); 5 6 const callback = useCallback(() => { 7 return count; 8 }, [count]); 9 return <div> 10 <h4>父组件:{count}</h4> 11 <Child callback={callback}/> 12 <button onClick={() => setCount(count + 1)}>点我+1</button> 13 </div>; 14} 15 16function Child({ callback }) { 17 const [count, setCount] = useState(() => callback()); 18 useEffect(() => { 19 setCount(callback()); 20 }, [callback]); 21 return <div> 22 子组件:{count} 23 </div> 24} 25 26export default Parent;

需要说明的是,React.memo和 React.useCallback一定记得配对使用,缺了一个都可能导致性能不升反“降”,毕竟无意义的浅比较也会消耗一些性能。

2.5 useRef

在React中,我们使用Ref来获取组件的实例或者DOM元素,我们可以使用两种方式来创建 Ref:createRef和useRef,如下所示。

1import React, { useState, useRef } from 'react' 2 3function App(){ 4 const [count, setCount] = useState(0) 5 const counterEl = useRef(null) 6 7 const increment = () => { 8 setCount(count + 1) 9 console.log(counterEl) 10 } 11 12 return ( 13 <> 14 Count: <span ref={counterEl}>{count}</span> 15 <button onClick={increment}>点我+</button> 16 </> 17 ) 18} 19

2.6 useReducer

useReducer的作用类似redux中的功能,相较于useState,useReducer适合一些逻辑较复杂且包含多个子值的情况。reducer接受两个参数,第一个参数是一个reducer,第二个参数是初始 state,返回值为最新的state和dispatch函数。

按照官方的说法,useReducer适合用于复杂的state操作逻辑,嵌套的state的对象的场景。下面是官方给出的示例。

1import React, { useReducer } from 'react'; 2 3function Reducers () { 4 const initialState={count:0} 5 const [count,dispatch] = useReducer((state,avtion) => { 6 switch(avtion.type) { 7 case 'add': 8 return state+1; 9 case 'minus': 10 return state-1 11 default: 12 return state 13 } 14 },0) 15 return ( 16 <div> 17 <div>{count}</div> 18 <button onClick={() => {dispatch({type: 'add'})}}></button> 19 <button onClick={() => {dispatch({type: 'minus'})}}></button> 20 </div> 21 ) 22} 23export default Reducers 24

2.7 useImperativeHandle

useImperativeHandle 可以让开发者在使用 ref 时自定义暴露给父组件的实例值。其意思就是,子组件可以选择性的暴露一些方法给父组件,然后隐藏一些私有方法和属性,官方建议,useImperativeHandle最好与 forwardRef 一起使用。

1import React, { useRef, forwardRef, useImperativeHandle } from 'react' 2 3const App = forwardRef((props,ref) => { 4 5 const inputRef = useRef() 6 7 useImperativeHandle(ref,()=>({ 8 focus : () =>{ 9 inputRef.current.focus() 10 } 11 }),[inputRef]) 12 return <input type="text" ref={inputRef}/> 13}) 14 15export default function Father() { 16 const inputRef = useRef() 17 18 return ( 19 <div> 20 <App ref={inputRef}/> 21 <button onClick={e=>inputRef.current.focus()}>获取焦点</button> 22 </div> 23 ) 24} 25 26

在示例中,我们通过 useImperativeHandle 将子组件的实例属性输出到父组件,而子组件内部通过 ref 更改 current 对象后组件不会重新渲染,需要改变 useState 设置的状态才能更改。

除了上面介绍的几种Hook API之外,React Hook常见的API还包括useLayoutEffect、useDebugValue。

自定义 Hook

使用Hook技术,React函数组件的this指向、生命周期逻辑冗余的问题都已得到解决,不过React开发中另一个比较常见的问题,逻辑代码复用仍然没有得到解决。如果要解决这个问题,需要通过自定义Hook。

所谓的自定义Hook,其实就是指函数名以use开头并调用其他Hook的函数,自定义Hook的每个状态都是完全独立的。例如,下面是使用自定义Hook封装axios实现网络请求的示例,代码如下。

1import axios from 'axios' 2import { useEffect, useState} from 'react'; 3 4const useAxios = (url, dependencies) => { 5 6 const [isLoading, setIsLoading] = useState(false); 7 const [response, setResponse] = useState(null); 8 const [error, setError] = useState(null); 9 10 useEffect(() => { 11 setIsLoading(true); 12 axios.get(url).then((res) => { 13 setIsLoading(false); 14 setResponse(res); 15 }).catch((err) => { 16 setIsLoading(false); 17 setError(err); 18 }); 19 }, dependencies); 20 return [isLoading, response, error]; 21} 22 23export default useAxios; 24

在上面的代码中,我们使用React已有的API实现自定义Hook的功能。而具体使用时,自定义Hook的使用方法和React官方提供的Hook API使用上类似,如下所示。

1function App() { 2 let url = 'http://api.douban.com/v2/movie/in_theaters'; 3 const [isLoading, response, error] = useAxios(url, []); 4 5 return ( 6 <div> 7 {isLoading ? <div>loading...</div> : 8 (error ? <div> There is an error happened </div> : <div> Success, {response} </div>)} 9 </div> 10 ) 11} 12 13export default App; 14

可以发现,相比于函数属性和高阶组件等方式,自定义Hook则更加的简洁易读,不仅于此,自定义Hook也不会引起之组件嵌套地狱问题。

虽然React的Hooks有着诸多的优势。不过,在使用Hooks的过程中,需要注意以下两点:

  • 不要在循环、条件或嵌套函数中使用Hook,并且只能在React函数的顶层使用Hook。之所以要这么做,是因为React需要利用调用顺序来正确更新相应的状态,以及调用相应的生命周期函数函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。
  • 只能在React函数式组件或自定义Hook中使用Hook。

同时,为了避免在开发中造成一些低级的错误,可以安装一个eslint插件,命令如下。

1yarn add eslint-plugin-react-hooks --dev 2

然后,在eslint的配置文件中添加如下一些配置。

1{ 2 "plugins": [ 3 // ... 4 "react-hooks" 5 ], 6 "rules": { 7 // ... 8 "react-hooks/rules-of-hooks": "error", 9 "react-hooks/exhaustive-deps": "warn" 10 } 11} 12 13
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )