Skip to content

React Hooks食用指南(二): Use Hooks #7

Open
@jsonz1993

Description

@jsonz1993

第二篇我们先来介绍这次的主角,几个比较常用的Hooks,本篇只涉及到几个常用的:useState, useEffect, useRef, useReducer

useState

可以理解为是ClassC中的setState,他也是官方一直宣传的赋予Hooks state的方法。
但是使用上最大的不同就是他每次setState不会自动帮你做merge,而是全替换,所以是自己封装一个方法,还是每次都用函数式更新,还是直接用useReduce(下面会讲到)看个人喜欢

import React, { useState } from 'react'
const App = () => {
  const [state, setState] = useState({
    name: 'jsonz',
    email: '[email protected]'
  })

  return (
    <>
      <p>{state.email}</p>
      <input 
        value={state.name} 
        // 这里用到函数式更新去手动merge
        onChange={({target: {value}})=> setState(prev => {...prev, {name: value }})}
      />
    </>
  )
}

useEffect

处理一些副作用,或者用来模拟ClassC的生命周期,第二个参数为监听依赖执行的参数。useEffect是我目前遇到最多问题,最骚的一个API

import React, { useHook } from 'React'
const App = () => {
  useHook(()=> {
    console.log('componentDidMount')
    return () => console.log('componentWillUnmount')
  }, [])
}

有个简单的认识之后,我们开始来写demo了


回顾我们第一篇中的例子 React Hooks食用指南(一),如果说我们的需求就是要实现读取实时数据,在触发console的时候输出当前最新的Name,而不是去捕获点击时的值(classC最开始的情况),那要怎么实现?

可能有人想到,既然用props传进来的参数会被保存下来,那我直接用state来保存props不就可以有自己的状态?
live demo1

hook2_demo1

首先这种想法其实是很危险的,这种写法会导致每次props变更都渲染两次,造成不必要的性能浪费。

The function passed to useEffect will run after the render is committed to the screen

引用文档的话,Effect一般都是一些没必要在渲染之前处理的东西,所以就统一放到渲染后再执行。这就相当于在Class.ComponentDidUpdate中写个setState
如果说是业务里面不可避免的需要根据props更新state的值的话,答应我善用useEffect的第二个参数。

再来说结论,用state和用props本质上对这个例子来说是没有区别的.

我们在第一篇的时候就有提到,FunC每一次渲染都会保存当前的状态。

为了便于理解,大家可以把它想成是一个时间轴,随着每次执行而前进,我们点击的时候就把这一帧的状态都给保存下来,所以console的时候输出的也是这一帧的数据。

按照你说的,会保存当前组件的帧,那我把这个值提到组件外面,再用useEffect去更新他,不就可以了?
hook2_demo2

emmm 这在需求实现的角度说确实可行,但是一般我们都不会把组件自身依赖的东西放到外面毫不相干的地方,不好维护。
比起这个方案,我们可以使用useRef来更好的处理类似的问题。


useRef

useRefReact.ref最大的不同是,useRef不仅仅只是用于dom引用访问子组件,可以把它理解为一个脱离时间轴的盒子,不管什么时候访问他都只能拿到最后一次保存的那帧数据,而且改变Ref不会引起reRender。经常会被用在值引用或者setInterval、setTimeout等场景

import React, { useRef, useEffect } from 'react'
const App = (props) => {
  const { name } = porps
  const lastName = useRef(name)
  const inputRef = useRef(null)
  useEffect(() => {
    lastName.current = name
  }, [name])
  
  return (
    <input value={name} ref={inputRef} />
  )
}

用ref来保存最新传入的props值 live demo2
hook2_demo3

上面我们讲了一个setTimeout的例子,接下来我们讲setInterval的


需求1:实现一个setInterval,每秒+1

我们可以看看在线的demo live demo3

classC中规中矩CDM(componentDidMount)的时候设置计时器,CWUM(ComponentWillUnMount)的时候清掉我们的计时器引用

我们的FunC直接翻译ClassC来,用useEffect模拟两个生命周期
但是很奇怪的是,FunC,一直输出的是 1,而每次都有console意味着这段计时器是生效的。

hook2_demo4

很好理解,首先这个Effect的依赖是一个空数组,也就是只会在CDMComponentWillUnMount执行,而当CDM的时候,捕获到的count是0,所以后面setInterval每一次执行的时候,count都只会是0。

既然知道问题了,我们来改下代码,把count加到Effect的依赖中。 live demo4 选择function mode为2

hook2_demo5

虽然需求是实现了,但这肯定不是我们想要的结果。因为每一次count变动,都会清除掉计时器,再重新起一个计时器,这肯定是不能忍的。

一般这种情况,我们可以用 setState 的第二种调用方法,传个函数给他去解决,当然对于更加复杂的数据,还是比较建议使用useReducerlive demo5 选择function mode为3

这个例子最主要想要说明的是 useEffect执行时捕获到的是当时的数据,这一点没理清楚很容易在业务上踩坑

至此需求1算是完成,接下来我们来看第二个需求:实现时间间隔可变

也就是我们现在都是写死 setInterval(xxx, 1000),现在把这个delay改为用户输入可控的形式

live demo6

先看Class的方案,这里先在 CDM设置计时器,在CWUM清除计时器引用,并且在CDU里面判断如果delay不同的话,就清除并重新设置定时器
这一套下来...三个生命周期 四十几行代码跑不掉😭
hook2_demo6

回头我们来一步一步说一下用Hooks实现的解决方案
首先,我们的useEffect里面启动一个计时器,返回清除计时器引用的函数,并且需要对delay做一个依赖,delay变化就重新执行刚才的操作,然后...然后没啦!
hook2_demo7 FunctionInterval2

用不到一半的代码行数实现了ClassC几十行的代码,并且可读性高容易理解,这就是Hooks的魅力🍋

我们还可以把这个功能封装成一个Hooks,没错不带任何jsx的hook,这用class是几乎不敢想的。
hook2_demo8

如果是要用class的方式去把这个功能封装成一个独立的组件,我能想到的只有类HOC。
HOC属于ClassC的一个不错的实践,但是如果用的太泛滥又会暴露一些问题,比如慢慢的你会陷入封装(嵌套)地狱,并且你的dom可能会是一个金字塔...

hook2_demo7
这是项目随便截的一个鉴权错误的页面,页面内容就只有一个icon和一段文案...这在React中是及其常见的,大家可以去瞄下自己的项目 (感谢React.Fragment)

从这里其实我们就可以窥视出Function Component with Hooks比Class Component的优势在哪里。

本质是函数,所以意味着组合更加简单方便,逻辑更容易抽处理复用,不用再去写各种 super(props) 和生命周期,也不用写太多的模版代码!


useReducer

最后我们再简单来介绍一下 useReducer。用过redux的肯定对这个不陌生,引用redux对reducer的介绍

Reducers 指定了应用状态的变化如何响应 actions

简单可以理解成他可以让你更灵活的处理 state,而且更强大

import { useReducer } from 'react'

const initState = {
  name: 'jsonz',
}
function reducer(state, action) {
  const { type, payload } = action
  switch (type) {
    case 'updateName':
      return {
        ...state,
        name: payload,
      }
    default: xxx
  }
}
const App = () => {
  const [state, setState] = useReducer(reducer, initState)
  return <>{state.name}</>
}

在线小demo,reducer可以解决我们使用useState.setState,state会被全覆盖的问题。
live demo7

至此对常用的Hooks应该有个大概的了解,下一篇讲一下useEffect的使用,以及近期对Hooks实践中踩到的坑

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions