文章目录
前言
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);
};
但是非常不建议把代码写成这个样子,不好维护。