You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>
});

额外优化建议

  1. 封装子组件:把单个公告项抽成独立的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
      ));
    }} 
  />
));
  1. 替换原生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} ... />
  1. 缓存事件处理函数:用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 09:06:01