【react框架】React18 Hooks:我的避坑指南(上)

前言

React的hooks我的理解是分为两个类型,一个是原生的hooks,一个是后期我们开发人员自己封装的自定义hooks。

原生的hooks提供了React的基本功能,且使用起来也非常顺手。但我在日常开发中发现了这些hooks在使用上有非常多的注意事项,或者叫做坑,而且有些还挺容易忘记的,所以写个博客记录下。

下篇地址

文章更新记录:简化篇幅,优化记忆点

对于一些平时开发根本不会这样做,但是面试会问到的“坑点”,我用(非必须)来标识标题,不是为了准备面试的人了解一下就行。


useState

这里有个温馨提示,严格来说useState存储的数据叫做“状态”,状态就是变化的数据。

更新了后想立马取到状态

在set中用函数可以拿到:

const [str, setStr] = useState('111')

setStr((data)=>{
	console.log('str', data) // 可以拿到最新值
	return '222' // 一定要返回一个修改的值
})

不是所有变量都需要useState

对于没有使用在视图上的状态,我们可以直接定义普通变量。有两个好处:

  • 不会触发render
  • 不用担心异步取值的问题

可用函数初始化一个状态

当一个初始化的状态非直接定义的情况下,可以在useState里用函数去定义它:

const [obj, setObj] = useState(()=>{
	// ...
	return {}
})

合并更新问题

import { useState, useEffect } from 'react'

function useStateDemo() {
    const [value, setValue] = useState(100)

    function clickHandler() {
        // 情况1. 传入常量,state 会合并
        setValue(value + 1)
        setValue(value + 1)
        console.log(1, value) // 100,拿不到最新的值,但虽然setValue两次,但是只算最后一次
        // 情况2. 传入函数,state 不会合并
        setValue(value => value + 1) // 拿到最新值101+1
        setValue(value => value + 1) // 拿到最新值102+1
        console.log(2, value) // 100 拿不到最新值
	}

    return <div>
        <span>{value}</span> // 情况1 点击后展示101 情况2 点击后展示103
        <button onClick={clickHandler}>increase1</button>
    </div>
}

export default useStateDemo

听说在17和之前的版本中用定时器包裹setState会使得state不会合并更新(这个有待验证,了解即可):

setTimeout(() => {
    setValue(value + 1)
    setValue(value + 1)
    console.log(1, value) // 100 但视图是102
})

不过一般真实代码中,我们也不会去同步连续写两次setState,一般都是会重新申明一个变量,处理完后一次性setState。

直接修改值的情况(非必须)

const [obj, setObj] = useState({ a: 1 })

const changeObj = () => {
  console.log(obj.a)
  obj.a = 3
  console.log(obj.a)
}
<span onClick={changeObj}>{obj.a}</span>

点击事件一直触发,会打印1,3,3,3,3,3…但是页面上的还是1。值类型也是一样的。虽然说没人会这么写,但是面试会问。

set时需要一个新的变量,否则视图层不更新

set的时候其实需要我们传入一个新的变量。

const [obj, setObj] = useState({ a: 1 })

const changeObj = () => {
  obj.a = 3
  setObj(obj)
  console.log(obj)
}

虽然打印的是3,但是页面上的还是1。你需要自己先处理成一个新的变量,再set。

在平时写代码的时候可能会觉得麻烦,我们可以通过immer这样的第三方库帮忙处理。当然原始方法能够更加熟悉原生api返回值。


props限制

props没有对应的hooks,不过非TS写法容易把props定义的没有规范,可以借用prop-types来限制:

import React from 'react'
import PropTypes from 'prop-types'

export default function Small(props) {
    const { name, age } = props;
    return (
        <div>名字{name}-年龄{age}</div>
    )
}

// 对标签属性进行类型、必要性的限制
Small.propTypes = { // 这个属性react会捕捉到,然后下面的PropTypes是个库对象,帮我们处理限制的
    name: PropTypes.string.isRequired, // 限制必传,且为字符串
    age: PropTypes.number,// 限制为数值
    speak: PropTypes.func,// 限制为函数
}
//指定默认标签属性值
Small.defaultProps = {
    age: 18
}

useRef

循环存储多个dom的引用

const divRefs = useRef([]); // 创建一个 Ref 数组来存储每个dom的引用
 
<div>
    {list.map((item, index) => {
        return  <form ref={(el) => (divRefs.current[index] = el)} key={item.id}></form>
    })}
</div>

更新不会触发render

const myRef = React.useRef('1')

// 获取与修改myRef.current = xxx

由于他不会触发render,所以值修改了并显示在页面,页面是不会更新的。

个人认为直接用普通变量声明的方式代替useRef更好,但是当有闭包陷阱(useEffect会讲)时还需要靠他。

隔离实例

我们都知道不要在组件定义外面去声明变量,会造成变量污染的问题。所以当我们去实例化一些第三方库的时候,要在组件定义的内部去获取它。

例如使用了postal这个发布订阅工具库时:

let listenBus = null // 错误示范
export default function Small(props) {
    const listenBus = useRef(null) // 正确示范
    // ...
    useEffect(()=>{
        // 订阅
	    return () => {
			// 销毁
		}
    }, [])
}

这样这个组件被多次引用的时候,每个listenBus都是独立的。这是很多React18新手容易犯的错误。

在非受控组件中建议使用

todo:这里我后面更新一下对应文章,然后链接过去好理解一些。


useEffect

看了神光小册才明白这个为什么被翻译成副作用了,因为在执行useEffect,额外执行了里面的函数内容,这些函数内容就是副作用。

可以书写多次

新手第一次接触的时候可能以为只能写一次,其实可以写多次:

useEffect(() => {
    console.log('componentDidMount的执行');
}, [])
useEffect(() => {
    console.log('componentDidMount的执行');
}, [])
useEffect(() => {
    console.log('componentDidMount的执行');
}, [])

重新赋相同的值不会触发

useEffect监听的变量被赋予相同的值时,是不会触发的。这点要注意。

闭包陷阱

react有一个闭包陷阱的问题

例子1:

const [value, setValue] = useState(0)
useEffect(()=>{
	const timer = setInterval(() => {
      console.log(value)
    }, 1000);
    return () => clearInterval(timer);
}, [])
// 后面jsx绑定一个点击让value加1的事件

不断的点击value加1,结果却是每一秒打印的都是0。

例子2:

const [count, setCount] = useState(0);

const add = () => {
  setCount(count + 1);
};

const fn = () => {
  setTimeout(() => {
    console.log(count);
  }, 3000);
};
useEffect(()=>{
  fn()
}, [])

先触发fn,然后不断触发add,打印0

这是因为内部函数在被定义的时候,里面拿到的值是被定义时的状态,所以内部函数被执行时,是获取不到最新状态的。

如何解决例子里的问题?

解决方法1:使用useEffect的第一个参数即可处理

useEffect(() => {
    const timer = setInterval(() => {
        console.log(value)
    }, 1000)

    return () => {
        clearInterval(timer) // 第二步,监听每触发一次就清掉上次的定时器,这样就能取消闭包
    }
}, [value]) // 第一步监听value的变化

所以你会发现有时候你只想在useEffect中监听某个变量的变化,但编辑器提示你还要多监听几个变量,也是为了防止闭包陷阱这个问题。

解决方法2:用useRef代替useState

const value = useRef(0)
useEffect(() => {
  const timer = setInterval(() => {
    console.log(value)
  }, 1000);
  return () => clearInterval(timer);
}, [])

const fn = () => {
  value.current++
}

写监听的依赖项要严谨

如果不写[],每次render的时候,useEffect都会被执行。

不要写一些奇奇怪怪的监听项,例如:

useEffect(() => {
	// ...
}, [23232, Date.now()])

前者毫无意义,后者每次render的时候都会让useEffect执行一遍。

18版本开发环境下初始化会多执行一次

例如:

useEffect(() => {
  console.log(1)
  return () => {
    console.log(2)
  }
}, [])

页面刚进入执行情况1,2,1

这是因为18版本的开发环境(打包后不会),这是react的设计,初始化时先模拟组件创建销毁动作,方便暴露问题。

跟踪依赖后,每一次变化前都会执行前一次return的函数(非必要)

结合上面的例子:

useEffect(() => {
  console.log(1)
  return () => {
    console.log(2)
  }
}, [count])

打印先是1,2,1。然后当count改变了,就会打印2,1,记住这个2是上一次useEffect触发时的return的函数!

内部不能直接执行async await函数

可以在useEffect里面定义aysnc函数再执行:

useEffect(() => {
  // 定义一个异步函数
  async function fetchData() {
    try {
      // 等待异步操作完成
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      // 处理数据
      console.log(data);
    } catch (error) {
      // 处理错误
      console.error('Error fetching data:', error);
    }
  }

  // 调用异步函数
  fetchData();
}, []); 

如果这个异步函数需要被复用,可以定义在useEffect外面。

处理监听上的错误提示

例如下面的代码:

const [allList , setAllList] = useState([])
const [list , setList] = useState([])

useEffect(()=>{
	list.forEach(item=>{
		// ...
		setAllList(list.concat(item))
	})
}, [allList])

这个时候编辑器可能就会提醒我们,list被用到了,也需要放入[allList]一起被监听,但我们知道,如果放入的话就会触发死循环,我们可以这样改写:

useEffect(()=>{
	list.forEach(item=>{
		// ...
		setAllList(list => list.concat(item)) // 作为函数参数传入就不会触发提示了
	})
}, [allList])

如何应对各种set

我们知道当我们的useState散落在各处的异步操作中会出现这样一种情况,例如:

let [count, setCount] = useState(0);
useEffect(() => {
  fn1();
  fn2();
}, []);
useEffect(() => {
  console.log("完成", count);
}, [count]);
const fn1 = () => {
  setTimeout(() => {
    setCount(count + 1);
  }, 1000);
};

const fn2 = () => {
  setTimeout(() => {
    setCount(count + 1);
  }, 1500);
};

这种情况count永远加不到2,原因可以详细看上面讲的useState篇幅。解决办法其实很简单,就用set返回函数的方式修改值就可以了:

let [count, setCount] = useState(0)
useEffect(() => {
  fn1();
  fn2();
}, []);
useEffect(()=>{
  console.log('完成', count);
}, [count])
const fn1 = () => {
  setTimeout(() => {
      setCount((data)=>{
          return data + 1
      })
  }, 1000);
};

const fn2 = () => {
  setTimeout(() => {
      setCount((data)=>{
          return data + 1
      })
  }, 1500);
};

但是非常不建议把代码写成这个样子,不好维护。


  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值