为何自定义usePrevious Hook首次点击按钮时previousValue与counter值相同?
自定义Hook usePrevious首次点击值异常的原因分析
问题现象
你实现的usePrevious Hook在首次点击按钮时,出现previousValue与counter值相同的情况,以下是你的代码:
import { useRef, useState } from "react"; import "./styles.css"; function usePrevious(value) { const currRef = useRef(); const previousValue = currRef.current; currRef.current = value; return previousValue; } export default function App() { const [counter, setCounter] = useState(0); const previousValue = usePrevious(counter); return ( <div className="App"> <h1>{counter}</h1> <h1>{previousValue}</h1> <h2 onClick={() => setCounter((prev) => prev + 1)}>btn</h2> </div> ); }
成因解析
这个问题的核心是你的usePrevious实现违反了React渲染阶段的纯函数原则,再加上React开发环境严格模式的特殊机制导致的:
1. 渲染阶段直接修改ref的副作用
你在usePrevious中,直接在组件渲染过程中修改currRef.current的值。而React要求组件渲染函数必须是纯函数——不能产生任何副作用(包括直接修改ref),因为渲染阶段的代码可能被React重复执行(比如严格模式下)。
2. 严格模式下的重复渲染检测
在React开发环境的严格模式中,为了帮助开发者发现无意义的副作用,React会对组件函数和effect执行两次。这会导致你的usePrevious逻辑被重复执行,从而意外覆盖ref的值:
首次渲染(两次执行组件函数)
- 第一次执行:
usePrevious(0),currRef.current初始为undefined,返回undefined,随后将currRef.current设为0。 - 第二次执行:
usePrevious(0),此时currRef.current已经是0(第一次执行时设置的),所以返回0,再次将currRef.current设为0。 - 最终页面显示
counter=0、previousValue=0,看起来两者相同。
首次点击后(两次执行组件函数)
- 状态更新后
counter=1,第一次执行:usePrevious(1),currRef.current是上一次的0,返回0,随后设为1。 - 第二次执行:
usePrevious(1),此时currRef.current已经是1(第一次执行时设置的),所以返回1,再次设为1。 - 最终页面显示
counter=1、previousValue=1,也就是你看到的两者相同的情况。
正确的usePrevious实现
应该使用useEffect在渲染完成后的effect阶段更新ref,确保只在渲染结束后执行一次,避免严格模式下的重复执行问题:
import { useRef, useEffect } from "react"; function usePrevious(value) { const ref = useRef(); // 仅在value变化且渲染完成后更新ref useEffect(() => { ref.current = value; }, [value]); return ref.current; }
正确实现的执行流程
- 首次渲染:
ref.current初始为undefined,渲染完成后useEffect将其设为0,返回undefined。 - 首次点击后:组件渲染时返回
ref.current的旧值0,渲染完成后useEffect将ref.current更新为1,页面显示1和0,符合预期。
内容的提问来源于stack exchange,提问作者Malek Essam
相关产品推荐
相关产品推荐

