React中自动调整尺寸的输入框初始宽度计算错误原因排查
自动调整宽度的文本输入框首次渲染宽度计算异常问题
实现代码
ResizableInput 组件
const ResizableInput = () => { const width = useTextRenderWidth({ text: currentName, className: 'text-lg font-display', }); return ( <input type={'text'} value={currentName} style={{ width }} onChange={({ target }) => setCurrentName(target.value)} /> ); };
useTextRenderWidth 钩子
const useTextRenderWidth = ({ text, className }) => { const [width, setWidth] = useState(0); const elem = useMemo(() => { const span = document.createElement('span'); span.style.position = 'fixed'; span.style.whiteSpace = 'pre'; span.style.top = '-100%'; document.body.appendChild(span); return span; }, []); useEffect(() => { elem.innerText = text; }, [text]); useEffect(() => { if (!isNil(className)) { elem.className = className; } }, [className]); useEffect(() => { setWidth(elem.offsetWidth); }, [text, className]); return width; };
遇到的问题
首次渲染时输入框宽度计算略小于所需值,但编辑内容(如添加再删除空格,无实质内容变化)或Vite热重载后,宽度计算恢复正常。DevTools中生成的span元素宽度显示正确,不清楚首次渲染无法设置正确宽度的原因,想了解是否有可监听的事件来捕获元素宽度正确设置的时机。
解决方案及分析
问题根源
首次渲染时,设置className和text后立即读取offsetWidth,此时浏览器可能还未完成字体加载、样式计算和重排,导致获取的宽度不是最终渲染后的真实宽度。而后续编辑或热重载时,字体已经加载完成,样式也已生效,所以宽度计算正确。
解决方法
用
requestAnimationFrame延迟读取宽度
把读取宽度的逻辑放到浏览器下一次重绘周期执行,确保获取最终渲染后的宽度:useEffect(() => { const updateWidth = () => { setWidth(elem.offsetWidth); }; const rafId = requestAnimationFrame(updateWidth); return () => cancelAnimationFrame(rafId); }, [text, className]);监听字体加载完成事件
如果使用了自定义字体,可等待字体加载完成后再计算宽度,避免因字体未加载导致的宽度偏差:useEffect(() => { async function waitForFonts() { if ('fonts' in document) { await document.fonts.ready; setWidth(elem.offsetWidth); } else { // 降级方案:延迟一段时间再计算 setTimeout(() => setWidth(elem.offsetWidth), 100); } } waitForFonts(); }, [text, className]);合并逻辑减少异步顺序问题
把设置文本、样式和计算宽度的逻辑合并到同一个useEffect,确保前序操作完成后再读取宽度:useEffect(() => { elem.innerText = text; if (!isNil(className)) { elem.className = className; } const rafId = requestAnimationFrame(() => { setWidth(elem.offsetWidth); }); return () => cancelAnimationFrame(rafId); }, [text, className]);
可选优化
- 给输入框设置最小宽度,避免内容为空时输入框过窄;
- 在组件卸载时移除创建的span元素,避免内存泄漏:
useEffect(() => { return () => { document.body.removeChild(elem); }; }, [elem]);
内容的提问来源于stack exchange,提问作者k.meinkopf
相关产品推荐
相关产品推荐

