React Hooks 使用中遇到的坑
React Hooks 注意事项
- 注意事项一:useState 初始化值,只有第一次有效
import React, { useState } from 'react'
// 子组件
function Child({ userInfo }) {
// render: 初始化 state
// re-render: 只恢复初始化的 state 值,不会再重新设置新的值
// 只能用 setName 修改
const [ name, setName ] = useState(userInfo.name)
return <div>
<p>Child, props name: {userInfo.name}</p>
<p>Child, state name: {name}</p>
function App() {
const [name, setName] = useState('sheben')
const userInfo = { name }
return <div>
Parent
<button onClick={() => setName('社本')}>setName</button>
<Child userInfo={userInfo}/>
export default App
点击 button 后,我们可以看到父组件传递给子组件的 props 已经改变了,但是子组件中不会重新 setName。
利用这点我们可以把复杂的 state 初始化放到 useState 中:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
实际上 useState 声明的状态应该是函数组件内部的状态,如果和 props 有关,应该在 useEffect 中去改变。
如果一个数据和 props 有关,我们可以在 useMemo、useEffect 中处理这个数据,而不应该使用 useState,因为这样会破坏 React 的单向数据流。
使用 useEffect 和 useMemo 处理 props 的区别。useMemo 依赖的数据不改变,页面是不会重新渲染的。useEffect 中只要父组件重新渲染,子组件就会重新渲染。
- 注意事项二:capture value
import React, { useState } from 'react'
function App() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
console.log('You clicked on: ' + count);
}, 3000);
return (
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<button onClick={handleClick}>print count</button>
export default App
我们来看这个例子,先点击print countbutton,再点击count + 1button,控制台打印的还是之前的 count:0
我们再来看值捕获的另一个例子:
import React, { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const incr = () => {
setTimeout(() => {
setCount(count + 1);
}, 3000);
return (
<h1>{count}</h1>
<button onClick={incr}>+1</button>
export default App;
初始页面如下:
当我们在 3s 内快速点击 +1 button 5下,页面会发生什么变化?页面更新如下:
function Index(){
const [ num ,setNumber ] = React.useState(0)
const handerClick=()=>{
for(let i=0; i<5;i++ ){
setTimeout(() => {
setNumber(num+1)
console.log(num)
}, 1000)
return <button onClick={ handerClick } >{ num }</button>
这就是 React 的值捕获。
注意区分 capture value 和闭包陷阱
-
注意事项三:useEffect 依赖
[]
时的闭包问题(闭包陷阱)
React 的闭包陷阱指的是当 useEffect 的依赖为
[]
的时候,当 React 函数式组件重新执行,useEffect hooks 并不会重新执行,这时候它内部的变量依旧是上一次的变量,这就构成了“闭包陷阱” :
import {useState, useEffect} from 'react'
export default function App() {
const [count, setCount] = useState(0)
// 模拟 DidMount
useEffect(() => {
// 定时任务
// useEffect 中的 count 永远是第一次的 count
const timer = setInterval(() => {
console.log('setInterval...', count)
}, 1000)
// 清除定时任务
return () => clearTimeout(timer)
}, []) // 依赖为 [],re-render 不会重新执行 effect 函数
return (<>
<div>count: {count}</div>
<div><button onClick={() => setCount(count+1)}>+1</button></div>
我们点击
+1
按钮数次后,页面渲染已经成为 4,而控制台打印的依旧是 0。这就是闭包陷阱。
只有 useEffect 中才会存在闭包陷阱,因为我们重新执行函数式组件的时候,useEffect 不会被重新执行,这时它依赖的就是上一次的变量。
-
注意事项三:useEffect 依赖了「引用类型」可能会出现死循环(React 是通过
Object.is
来判断依赖是否改变的) - 注意事项四:使用 useCallback 缓存函数的时候,如果函数内部使用了外部的变量,则需要把这个变量加进依赖中,否则函数不会检测到该变量的改变。
如何解决 React Hooks 中的闭包陷阱?
主要有三种方式:
- 使用回调函数赋值
- 使用 useRef
- 使用 useReducer
我们来一个一个分析
使用回调函数
当我们使用 setState 时,新的 state 如果是通过计算旧的 state 得出,那么我们可以将回调函数当作参数传递给 setState。该回调函数将接收先前的 state,并返回一个更新后的值:
import React, { useState, useEffect, useRef } from 'react'
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount((c) => c + 1)
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
export default App
使用 useRef
同一个 ref 在 React 组件的整个生命周期中只存在一个引用,因此通过 current 永远是可以访问到引用中最新的函数值,不会存在闭包陈旧值的问题。
import React, { useState, useEffect, useRef } from 'react'
function App() {
const [count, setCount] = useState(0)
const countRef = useRef(0)
// 模拟 DidMount
useEffect(() => {
// 定时任务
const timer = setInterval(() => {
console.log('setInterval...', countRef.current)
setCount(countRef.current++)
}, 1000)
// 清除定时任务
return () => clearTimeout(timer)
}, []) // 依赖为 [],re-render 不会重新执行 effect 函数
return <div>count: {count}</div>
export default App
使用 useReducer
利用 useReducer 获取的
dispatch
方法在组件的生命周期中保持唯一引用,并且总是能操作到最新的值:
import {useEffect, useReducer} from 'react'
const initialCount = 0;
function reducer(count, action) {
switch (action.type) {
case 'increment':
return count + 1
case 'decrement':
return count - 1
default:
throw new Error();
function App() {
const [count, dispatch] = useReducer(reducer, initialCount);
useEffect(()=>{
setInterval(() => {
dispatch({type:'increment'})
}, 1000);
},[])