React动态创建textarea输入失焦问题求助及代码优化建议
React动态textarea输入失焦问题解决
我是React新手,欢迎针对我的代码提出改进建议,我清楚自己可能未遵循部分最佳实践,希望能得到指导。目前遇到的问题是:每次对动态创建的textarea元素进行修改时,整个页面会重渲染,导致输入框失去焦点。基本只能每次输入一个字符,之后需要重新点击textarea才能继续输入。我明白这是因为announcementElements依赖announcements状态,当announcements发生变化时,announcementElements会被重渲染。
我的代码
import React from 'react'; import { FaThumbtack } from 'react-icons/fa6'; import { nanoid } from 'nanoid'; import { dateTime } from '../AppManager'; export default function Announcement({ announcements, setAnnouncements }) { function handleChange(event){ const { name, value } = event.target; setAnnouncements(oldAnnouncements => { return oldAnnouncements.map(old => { return old.id === name ? {...old, info: value, time: dateTime} : old; }) }) } const announcementsElement = announcements.map(announcement => { return <div key={nanoid()}> <textarea readOnly={false} className="announce" id={announcement.id} name={announcement.id} value={announcement.info} onChange={handleChange} /> <label htmlFor={announcement.id} style={{justifyContent: announcement.pinned ? "space-between" : "flex-end"}}> {announcement.pinned && <FaThumbtack className='announcepinned' />} <span className='announcetime'>{announcement.time}</span> </label> </div> }); React.useEffect(() => { function resize(){ const announceText = document.getElementsByClassName("announce"); for (let p = 0; p < announceText.length; p++) { const ann = announceText[p]; const height = ann.scrollHeight > 120 ? 120 : ann.scrollHeight; ann.style.height = height + "px"; } } document.querySelector(".announcement-wrapper").addEventListener("DOMNodeInserted", resize); return () => { document.querySelector(".announcement-wrapper").removeEventListener("DOMNodeInserted", resize); }; }, []); return ( <section> <div className="announcement" id="announcement"> <h2>Announcements</h2> <div className="announcement-wrapper"> {announcementsElement} </div> <div id="add1"> <button id="add" className="download update">Add</button> </div> </div> </section> ) }
Announcement数组结构
[ { "id": "etmlkmlekrmpe", "info":"Lorem ipsum dolor sit amet", "pinned": false, "time":"20/12/23 10:16:23" }, { "id": "okoikowekl", "info":"Lorem ipsum, dolor sit amet consectetur.", "pinned": false, "time":"21/12/23 10:16:23" }, { "id": "oolkmuhyuf", "info":"Lorem ipsum, dolor sit amet consectetur", "pinned": true, "time":"13/12/23 10:16:23" }, { "id": "kmkmlmklmkl", "info":"Lorem ipsum, dolor sit amet", "pinned": false, "time":"25/12/23 10:16:23" } ]
尝试过的useRef方案(未解决)
const announceRef = React.useRef([]); announceRef.current = announcements; const announcementsElement = announceRef.current.map(announcement => { return <div key={nanoid()}> <textarea readOnly={false} className="announce" id={announcement.id} name={announcement.id} value={announcement.info} onChange={handleChange} /> <label htmlFor={announcement.id} style={{justifyContent: announcement.pinned ? "space-between" : "flex-end"}}> {announcement.pinned && <FaThumbtack className='announcepinned' />} <span className='announcetime'>{announcement.time}</span> </label> </div> });
问题根源及解决方法
核心问题:不稳定的Key值
你给每个动态生成的div用nanoid()作为key,这会导致每次组件重渲染时,所有div的key都被替换成新值。React会把这些元素当成全新组件销毁重建,直接导致输入焦点丢失。
解决:使用固定唯一的id作为key
直接用announcement.id代替nanoid(),这个id是数组中每个项的唯一标识且不会变化,React能正确识别每个元素的身份,重渲染时不会销毁重建,焦点就能正常保留:
const announcementsElement = announcements.map(announcement => { return <div key={announcement.id}> {/* 替换nanoid()为announcement.id */} <textarea readOnly={false} className="announce" id={announcement.id} name={announcement.id} value={announcement.info} onChange={handleChange} /> <label htmlFor={announcement.id} style={{justifyContent: announcement.pinned ? "space-between" : "flex-end"}}> {announcement.pinned && <FaThumbtack className='announcepinned' />} <span className='announcetime'>{announcement.time}</span> </label> </div> });
额外优化建议
- 封装子组件:把单个公告项抽成独立的
AnnouncementItem组件,让代码更模块化,也能利用React的组件优化机制:
function AnnouncementItem({ announcement, onUpdate }) { const handleChange = (e) => { onUpdate(announcement.id, e.target.value); }; return ( <div key={announcement.id}> <textarea className="announce" id={announcement.id} value={announcement.info} onChange={handleChange} /> <label htmlFor={announcement.id} style={{justifyContent: announcement.pinned ? "space-between" : "flex-end"}}> {announcement.pinned && <FaThumbtack className='announcepinned' />} <span className='announcetime'>{announcement.time}</span> </label> </div> ); }
父组件中调用:
const announcementsElement = announcements.map(announcement => ( <AnnouncementItem key={announcement.id} announcement={announcement} onUpdate={(id, value) => { setAnnouncements(old => old.map(item => item.id === id ? {...item, info: value, time: dateTime} : item )); }} /> ));
- 替换原生DOM操作:你用
DOMNodeInserted事件调整textarea高度的方式,可以换成React的ref+useEffect实现,更符合React的编程模式:
// 在AnnouncementItem组件内 const textareaRef = React.useRef(null); React.useEffect(() => { if (textareaRef.current) { const height = Math.min(textareaRef.current.scrollHeight, 120); textareaRef.current.style.height = `${height}px`; } }, [announcement.info]); // 给textarea添加ref <textarea ref={textareaRef} ... />
- 缓存事件处理函数:用
useCallback缓存handleChange函数,避免父组件重渲染时频繁创建新函数,减少子组件不必要的重渲染:
const handleChange = React.useCallback((event) => { const { name, value } = event.target; setAnnouncements(oldAnnouncements => { return oldAnnouncements.map(old => { return old.id === name ? {...old, info: value, time: dateTime} : old; }) }); }, [setAnnouncements]);
内容的提问来源于stack exchange,提问作者Joshua
相关产品推荐
相关产品推荐

