React 调试实录:当输入框的值总是"叛逆"地重置
React 调试实录:当输入框的值总是”叛逆”地重置
大家好!今天想和大家分享一个最近在开发 React 应用时遇到的有趣 Bug。你是否也曾遇到过这样的情况:一个简单的输入框,你明明输入了内容,但它却像有自己的想法一样,瞬间恢复到了原来的值?如果你也抓耳挠腮过,那这篇文章可能会给你一些启发。
问题现象:挥之不去的”初始值”
在我们的图片编辑工具中,有一个控制面板 (CanvasControls),允许用户在裁剪模式下手动输入图片的宽度和高度。我们使用了 styled-components 创建了一个看起来很酷的胶囊状输入框 (CapsuleInput)。
然而,奇怪的事情发生了:每当用户尝试在宽度或高度输入框里输入数字时,输入框的值会立刻被重置回图片加载时的原始尺寸。就好像用户什么都没输入一样。
初步排查:确认基础逻辑
面对这种现象,我们首先快速检查了基础的事件处理:CapsuleInput 的 value 属性确实绑定到了组件的 width 和 height 状态,并且 onChange 事件处理函数也已正确添加,用于在用户输入时更新这些状态。
1 |
|
确认了基本的事件绑定和状态更新逻辑无误后,我们排除了是简单遗漏 onChange 导致的低级错误。但问题依旧存在,输入值还是会被重置。这说明问题隐藏得更深。
深入探究:useEffect 与不必要的重渲染
既然简单的状态更新没问题,那问题一定出在组件渲染周期的其他环节。我们再次审视 CanvasControls 组件的代码,注意到了这个 useEffect:
1 |
|
这个 useEffect 的作用是在 selectedImage(用户选择的图片)变化时,获取图片的原始尺寸,并用它来设置 width 和 height 状态的初始值。
疑点来了:useEffect 的依赖项数组包含了 getNaturalSize。这意味着,如果 getNaturalSize 函数的引用发生变化,这个 useEffect 就会重新执行。
难道用户每次输入导致状态更新,进而触发重新渲染时,getNaturalSize 的引用也变了?
我们赶紧查看了 getNaturalSize 的来源——自定义 Hook useImageSize (src/hooks/useImageContextHooks.ts):
1 |
|
真相大白! 问题就出在这里。getNaturalSize 函数是在 useImageSize Hook 内部直接定义的。这意味着每次 CanvasControls 组件渲染(包括我们输入数字触发 setWidth 或 setHeight 导致的状态更新后的重新渲染),useImageSize Hook 都会运行,从而创建一个全新的 getNaturalSize 函数实例。
因为 getNaturalSize 的引用在每次渲染时都不同,所以依赖于它的 useEffect 每次都会执行。结果就是,我们刚刚通过 onChange 更新的 width 或 height 状态,马上就被 useEffect 内部的 setWidth(naturalWidth) 和 setHeight(naturalHeight) 给覆盖回了图片的原始尺寸!这就是输入被重置的根本原因。
最终解决方案:useCallback 登场
要解决这个问题,我们需要确保 getNaturalSize 函数的引用保持稳定,除非它的依赖项(这里是 imageRef)真的发生了变化。这正是 useCallback Hook 的用武之地。
我们修改 useImageSize Hook,用 useCallback 来包裹 getNaturalSize 和 getDisplaySize:
1 |
|
修改后,getNaturalSize 函数的引用只会在 imageRef 变化时才更新。这样,在 CanvasControls 组件因输入而重新渲染时,getNaturalSize 的引用保持不变,useEffect 不会再次执行,我们输入的值也就不会被重置了!
再次测试,输入框终于”听话”了!
经验总结
这次调试过程再次提醒我们:
- React 的渲染机制:状态更新会导致组件重新渲染,理解这一点是解决复杂问题的基础。
- useEffect 的依赖项陷阱:务必谨慎处理 useEffect 的依赖项数组。如果依赖项是函数或对象,要特别注意它们的引用稳定性,否则可能导致 Effect 非预期地频繁执行。
- 自定义 Hook 的最佳实践:在自定义 Hook 中返回函数或对象时,应默认使用 useCallback 和 useMemo 进行记忆化,这可以有效避免下游组件因不必要的引用变化而产生性能问题或 Bug。
希望这个案例能帮助大家在未来的 React 开发中少走一些弯路!如果你也遇到过类似的”灵异”事件,欢迎在评论区分享你的故事和解决方案!