一个React Hook 小白的踩坑心得。

580 阅读7分钟

前言

什么是React Hook

官方说法:Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

为什么要使用Hook

  • 类组件组件逻辑难以复用,Hook 使你在无需修改组件结构的情况下复用状态逻辑
  • 组件将会更简洁,更利于拆分。
  • 更符合模块化开发的理念

React.FC (函数组件)

React.FC是函数式组件,是在TypeScript使用的一个泛型。FC是FunctionComponent的缩写,React.FC可以写成React.FunctionComponent

React.FC 类型格式要求必须具有一个返回值

1647840252(1).png

useState

useState 作为我们最常用的一个Hook,具有一个极其重要的特性React 会在重复渲染时保留这个 state,因为这个特性,函数组件拥有了自身的状态。可以实现类组件的一些逻辑

1647840664(1).png

useState唯一的参数就是初始值,这个初始值接受任何类型,只需要拥有返回值。

定义useState的类型

对于不使用Ts的同学,可以选择跳过这里。

我们通常使用如下方式去定义useState的类型。

正确类型:

1647840748(1).png

错误类型:

1647840839(1).png

但是通常情况下,TS会帮我们推断出useState的类型。

合并useState

相信很多和我一样刚开始使用React Hook的同学,会遇到一个变量去重复的声明一个useState。但是细想一下,其实很多的变量是可以合并起来,接受一个统一的类型。

未合并前:

1647841155(1).png

这样书写React Hook,如果在一个变量极其多的页面中,我们会发现我们会使用大量的useState,即使我们在每个useState上层都很详细的写出这个变量的含义,但是依然会给人一种很不舒服的感觉。

例如:我之前写的业务代码:

1647841248(1).png

所以我们需要对可以合并的useState 进行合并。

合并后:

1647841347(1).png

是不是看着顺眼了很多。其实这么去写。如果我们要根据一个事件去改变用户的信息。就会发现一个问题

我们先创建一个按钮,给按钮绑定一个事件,并且通过这个事件,去将用户的姓名更改

1647841791(1).png

可以思考一下。这样去改变是否可以更改成功?按照useState的官方说法,每次useState改变都会刷新视图,是否会刷新视图?

效果图

1647841770(1).png

触发更改事件。发现页面并没有变化。但是我们打印的userInfo却改变了。按照官方的说法,useState可以更改初始值,但是异步的形式,我们是不可能拿到更改后的值。为什么这里会出现这么一个情况?

深拷贝,浅拷贝?????

是的没错,就是因为深浅拷贝的原因,我们是通过浅拷贝,去给params赋予与useInfo的值,我们对params进行改变,其实一并也将userInfo给更改了。所以我们再去setUserInfo的视图没有刷新的根本原因就是:对于useState来说,其实userInfo并没有更新,所以不会刷新视图

解决方式:

我们可以通过ES6的扩展运算符来解决这个问题。将代码更改为:

1647842060(1).png

效果图:

1647842099(1).png

我们发现视图更新了,我们也没有拿到最新的值,一切又符合了useState的特性

useEffect

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

React Hook的出现,才使函数组件具有了生命周期,可以实现类组件的一切功能

componentDidMount生命周期

组件挂载阶段, 我们需要将useEffect的第二个参数传递为一个空数组。这样useEffect只会在页面挂载组件的阶段执行一次。

1647842452(1).png

效果图:

1647842476(1).png

我们可以看到,在组件的挂载阶段可以访问到变量了,说明componentDidMount阶段,其实页面已经加载结束,这个时候我们通常会需要数据来渲染页面,所以我们通常会在第二个参数为空数组的useEffect中去执行获取数据的方法

componentDidUpdate生命周期

1647842666(1).png

这个方式,通常用于我们需要监听一个变量的变化,根据这个变量的变化去做出不同的变化。第二个数组需要传递我们需要监听的变量,也就是依赖项,可以是多个,也可以是单个。

效果图:

1647842780(1).png

我们发现,其实更新阶段执行了2次。一次是在挂载阶段之后,一次是在变量更新时。试想一下,如果我们是根据这个值的变化去向服务端请求数据,重新渲染页面。那么,我们刚进入页面的时候,是不是就会发起2次相同的请求。这个是一个坑点,所以涉及到依赖变化需要发送请求的useEffect请慎重使用。

componentWillUnmount 组件卸载

1647843314(1).png

我们如果在页面中使用定时器,闭包,或者循环调用等类似的方法,我们需要在组件卸载的阶段去将这些清楚,避免内存泄漏的问题。

说到内存泄漏,俗称GA算法,是JS的一套垃圾回收机制,通过可达性分析去清楚无用的变量,释放内存。内存泄漏会导致浏览器的负荷增加,直到浏览器崩溃。最近在面试。。。。顺便复习下。嘿嘿嘿嘿

useRef

useRef(initialValue)  是一个内置的 React 钩子,它接受一个参数作为初始值并返回一个引用。引用是具有单个属性“current”的对象,可以访问和更改。

useRef 在我看来,主要是解决不同作用域共享一个状态的解决方案。

基础用法网上有很多,我在这里用我最近遇到的一个问题作为例子说明下。

下面是我写的一个有问题demo的代码:

Home.tsx文件,具体参数这里就不额外声明了,暂时用any替代

import React, { useEffect, useRef, useState } from 'react'
import { Table, Button } from 'antd'
import { Columns } from './Columns'

const data = [
    {
        id:1,
    },
    {
        id:2
    }
]

const Home:React.FC = () => {
    // 动态表头
    const [dynamicColumns, setDynamicColumns] = useState<any>([])
    // 用户信息
    const [userInfo, setUserInfo] = useState({
        name:'张三'
    })

    // 更改用户信息
    const changeUserInfo = () => {
        const data = {...userInfo}
        data.name = '李四'
        setUserInfo(data)
    }

    // 删除事件
    const delBtn = () => {
        console.log(userInfo,'userInfo');
    }

    // 处理表头
    const manageColumns = () => {
        const columns = Columns.map((i) => {
            if(i.dataIndex === 'option'){
                i.render = () => {
                    return <span onClick={delBtn}>删除</span>
                }
                return i
            }
            return i
        })
        setDynamicColumns(columns)
    }
    
    useEffect(() => {
        manageColumns()
    },[])


    return (
        <div>
            <h1>用户姓名:{userInfo.name}</h1>
            <Button
            onClick={changeUserInfo}
            >更改用户信息</Button>
            <Table
				rowKey={(i) => i.id}
				bordered
				columns={dynamicColumns}
				dataSource={data}
			/>
        </div>
    )
}

export default Home

Columns.tsx文件

export const Columns = [
    {
		title: '序号',
		dataIndex: 'index',
		align: 'center',
		key: 'index',
		render: (data, row, index) => {
			return index + 1
		},
	},
    {
		title: '操作',
		dataIndex: 'option',
		align: 'center',
		key: 'option',
	}
]

有兴趣的可以自行,运行下代码。

我们先点击按钮,更改userInfo的值,在点击表格中的删除按钮,打印userInfo的值。

效果图

1647845468(1).png

我们可以看到视图已经发生了变化,说明userInfo的值其实已经被修改了。但是我们点击删除,却打印的是初始值,这里不是因为useState异步的原因造成的。有疑问的可以多点击几次看下效果。

我理解的是因为作用域的不同,其实暴露出表头的文件其实是并没有共享到userInfo的变化。所以一直读取的是userInfo的初始值。

如果将表头放入到Home.tsx文件中,共享同一个作用域。并不会出现这个情况。

解决方案:

通过useRef去记录最新的值。

1647845817(1).png

效果图:

1647845843(1).png

不要纠结为什么要把表头拿到另外一个文件,这只是一个场景。或者不是表头呢?如果有更好的理解,可以在评论区下发表一下自己的看法。我并不认为我的理解是最正确的。

还剩下React 优化的Hook, React 局部状态管理的Hook, React 自定义Api的Hook。等明天再说吧。

下雨了,各位大佬加班愉快。。。。。。。。