React 调试实录:当输入框的值总是"叛逆"地重置

React 调试实录:当输入框的值总是”叛逆”地重置

大家好!今天想和大家分享一个最近在开发 React 应用时遇到的有趣 Bug。你是否也曾遇到过这样的情况:一个简单的输入框,你明明输入了内容,但它却像有自己的想法一样,瞬间恢复到了原来的值?如果你也抓耳挠腮过,那这篇文章可能会给你一些启发。

问题现象:挥之不去的”初始值”

在我们的图片编辑工具中,有一个控制面板 (CanvasControls),允许用户在裁剪模式下手动输入图片的宽度和高度。我们使用了 styled-components 创建了一个看起来很酷的胶囊状输入框 (CapsuleInput)。

然而,奇怪的事情发生了:每当用户尝试在宽度或高度输入框里输入数字时,输入框的值会立刻被重置回图片加载时的原始尺寸。就好像用户什么都没输入一样。

初步排查:确认基础逻辑

面对这种现象,我们首先快速检查了基础的事件处理:CapsuleInput 的 value 属性确实绑定到了组件的 width 和 height 状态,并且 onChange 事件处理函数也已正确添加,用于在用户输入时更新这些状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// CanvasControls.tsx
// ... (状态定义)

const handleWidthChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newWidth = parseInt(event.target.value, 10);
if (!isNaN(newWidth)) {
setWidth(newWidth); // 更新状态
} else if (event.target.value === '') {
setWidth(0);
}
};

// ... handleHeightChange 类似

return (
// ...
<CapsuleInput type="number" value={width} onChange={handleWidthChange} />
// ...
);

确认了基本的事件绑定和状态更新逻辑无误后,我们排除了是简单遗漏 onChange 导致的低级错误。但问题依旧存在,输入值还是会被重置。这说明问题隐藏得更深。

深入探究:useEffect 与不必要的重渲染

既然简单的状态更新没问题,那问题一定出在组件渲染周期的其他环节。我们再次审视 CanvasControls 组件的代码,注意到了这个 useEffect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// CanvasControls.tsx
import { useImageSize } from '../../../hooks/useImageContextHooks';

// ...

const CanvasControls: React.FC = () => {
// ...
const { getNaturalSize } = useImageSize(); // <--- 从自定义 Hook 获取函数
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);

useEffect(() => {
// 这个 Effect 用于在图片加载时,用图片的自然尺寸初始化输入框的值
if (selectedImage) {
const { width: naturalWidth, height: naturalHeight } = getNaturalSize();
if (naturalWidth > 0 && naturalHeight > 0) {
setWidth(naturalWidth); // <--- 这里会重置状态
setHeight(naturalHeight); // <--- 这里也会重置状态
}
}
}, [selectedImage, getNaturalSize]); // <--- 注意这里的依赖项!

// ... (事件处理函数等)
}

这个 useEffect 的作用是在 selectedImage(用户选择的图片)变化时,获取图片的原始尺寸,并用它来设置 width 和 height 状态的初始值。

疑点来了:useEffect 的依赖项数组包含了 getNaturalSize。这意味着,如果 getNaturalSize 函数的引用发生变化,这个 useEffect 就会重新执行。

难道用户每次输入导致状态更新,进而触发重新渲染时,getNaturalSize 的引用也变了?

我们赶紧查看了 getNaturalSize 的来源——自定义 Hook useImageSize (src/hooks/useImageContextHooks.ts):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/hooks/useImageContextHooks.ts
import { useContext } from 'react';
// ...

export const useImageSize = () => {
const { imageRef } = useImageRef();

// 每次 useImageSize Hook 执行(即 CanvasControls 渲染)时,
// 都会创建一个新的 getNaturalSize 函数实例!
const getNaturalSize = () => {
if (!imageRef?.current) {
return { width: 0, height: 0 };
}
return {
width: imageRef.current.naturalWidth,
height: imageRef.current.naturalHeight,
};
};

// ... (getDisplaySize 类似)
return { getNaturalSize, getDisplaySize };
};

真相大白! 问题就出在这里。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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/hooks/useImageContextHooks.ts
import { useContext, useCallback } from 'react'; // 导入 useCallback
// ...

export const useImageSize = () => {
const { imageRef } = useImageRef();

// 使用 useCallback 记忆化函数
const getNaturalSize = useCallback(() => {
if (!imageRef?.current) {
return { width: 0, height: 0 };
}
return {
width: imageRef.current.naturalWidth,
height: imageRef.current.naturalHeight,
};
}, [imageRef]); // 只有当 imageRef 变化时,才重新创建函数

const getDisplaySize = useCallback(() => {
// ... (类似处理)
}, [imageRef]);

return { getNaturalSize, getDisplaySize };
};

修改后,getNaturalSize 函数的引用只会在 imageRef 变化时才更新。这样,在 CanvasControls 组件因输入而重新渲染时,getNaturalSize 的引用保持不变,useEffect 不会再次执行,我们输入的值也就不会被重置了!

再次测试,输入框终于”听话”了!

经验总结

这次调试过程再次提醒我们:

  1. React 的渲染机制:状态更新会导致组件重新渲染,理解这一点是解决复杂问题的基础。
  2. useEffect 的依赖项陷阱:务必谨慎处理 useEffect 的依赖项数组。如果依赖项是函数或对象,要特别注意它们的引用稳定性,否则可能导致 Effect 非预期地频繁执行。
  3. 自定义 Hook 的最佳实践:在自定义 Hook 中返回函数或对象时,应默认使用 useCallback 和 useMemo 进行记忆化,这可以有效避免下游组件因不必要的引用变化而产生性能问题或 Bug。

希望这个案例能帮助大家在未来的 React 开发中少走一些弯路!如果你也遇到过类似的”灵异”事件,欢迎在评论区分享你的故事和解决方案!


React 调试实录:当输入框的值总是"叛逆"地重置
https://nanxfu.github.io/2025/05/04/叛逆地重置/
Beitragsautor
nanxfu
Veröffentlicht am
May 4, 2025
Urheberrechtshinweis