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

React输入框onChange触发后失去焦点问题求助

问题原因分析

你的输入框失去焦点、整个<tbody>重渲染的核心问题是Row组件定义在IngredientsTable函数内部。每次IngredientsTable的状态(比如ingredients)更新时,组件会重新执行,内部的Row组件会被重新创建。React会把这个新的Row组件视为完全不同的组件类型,因此会卸载旧的表格行,再挂载新的行——这直接导致输入框失去焦点,同时触发整个的重渲染。

另外你的addRow函数还存在闭包问题:更新ingredients时使用的rowsGenerated + 1可能不是最新的状态值,应该通过函数式更新来获取最新计数。

修复方案

方案1:将Row组件移到父组件外部

把Row组件从IngredientsTable内部抽离,避免每次父组件重渲染时重新创建该组件:

import React, { useState } from 'react';

// 将Row组件移到IngredientsTable外部
const Row = ({ rowId, name, proportion, onFieldChange }) => (
  <tr>
    <td>
      <input
        id={rowId + 'name'}
        type="text"
        value={name}
        onChange={(e) =>
          onFieldChange({
            id: rowId,
            field: 'name',
            value: e.target.value,
          })
        }
      />
    </td>
    <td>
      <input
        id={rowId + 'proportion'}
        type="text"
        value={proportion}
        onChange={(e) =>
          onFieldChange({
            id: rowId,
            field: 'proportion',
            value: e.target.value,
          })
        }
      />
    </td>
    <td>
      <button type="button">-</button>
    </td>
  </tr>
);

const IngredientsTable = () => {
  const [rowsGenerated, setRowsGenerated] = useState(3);

  const [ingredients, setIngredients] = useState({
    1: { id: '1', name: '', proportion: '' },
    2: { id: '2', name: '', proportion: '' },
    3: { id: '3', name: '', proportion: '' },
  });

  const onFieldChange = ({ value, id, field }) => {
    setIngredients((prevState) => ({
      ...prevState,
      [id]: { ...prevState[id], [field]: value },
    }));
  };

  const addRow = () => {
    // 使用函数式更新获取最新状态,解决闭包问题
    setRowsGenerated((prevCount) => {
      const newId = prevCount + 1;
      setIngredients((prevState) => ({
        ...prevState,
        [newId]: { id: newId.toString(), name: '', proportion: '' },
      }));
      return newId;
    });
  };

  return (
    <div className="ingredient_table">
      <table>
        <thead>
          <tr>
            <th style={{ width: '62%' }}>Name </th>
            <th style={{ width: '30%' }}>Proportion %</th>
            <th style={{ width: '8%' }}></th>
          </tr>
        </thead>
        <tbody>
          {Object.keys(ingredients).map((id) => (
            <Row
              key={`ingredient${id}`}
              rowId={id}
              name={ingredients[id].name}
              proportion={ingredients[id].proportion}
              onFieldChange={onFieldChange}
            />
          ))}
        </tbody>
      </table>
      <button className="ingredient_add_row_button" onClick={addRow}>
        Add row
      </button>
    </div>
  );
};

export default IngredientsTable;

方案2:用React.memo包裹内部Row组件(备选)

如果不想迁移Row组件,可通过React.memo缓存组件,仅当props变化时才重渲染:

// 在IngredientsTable内部修改Row组件定义
const Row = React.memo(({ rowId, name, proportion, onFieldChange }) => (
  <tr>
    {/* 输入框内容与之前一致 */}
  </tr>
));

同时需要按照方案1中的方式修复addRow的闭包问题。

额外说明
  • 输入框上的key属性无需单独设置,Row组件的key已经可以标识行的唯一性,React会通过该key追踪行的变化。
  • 修复闭包问题后,新增行时不会出现ID重复或错误的情况。

内容的提问来源于stack exchange,提问作者Eric P

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 01:20:20