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

React reducer更新文本框数组时焦点丢失的解决方案

问题

在React组件中维护了一个包含对象数组的state,通过循环该数组渲染多个文本框。但在文本框中输入单个字符后,焦点会被移除,需要重新点击才能继续输入。推测问题出在reducer中创建新数组返回导致重渲染,但不想使用Redux,且reducer内还有其他字段,请问如何更新状态同时保持文本框焦点?

原代码示例:

const reducer = (state, action) => {
  switch (action.type) {
    case 'taskUpdate': {
      const index = state.tasks.findIndex(task => task.id == action.payload.id);
      const newArray = [...state.tasks];
      
      newArray[index].task = action.payload.task;
      newArray[index].startDate = action.payload.startDate;
      newArray[index].endDate = action.payload.endDate;

      return { 
        ...state, 
        tasks: newArray, 
       } 
    };
    default:
      throw new Error();
  }
};

const TenderCreate = () =>  {

  const [state, dispatch] = useReducer(reducer, {
    tasks: [{ id: 1,
       task: '', 
       errorTask:'',
       startDate: '', 
       errorStartDateTime:'',
       endDate:'',
       errorEndDateTime: ''
      },
    ]
  });

  const TaskRow = ({ data }) => (
    <Form.Group as={Row} controlId="task1"  className="mb-3" >
      <Form.Control
        type="text"
        placeholder=""
        name="task"
        className={data.errorTask ? 'is-invalid' : ''}
        value={data.task}
        onChange={e => {
          dispatch({
            type: 'taskUpdate',
            payload: { 
              id: data.id, 
              task: e.target.value, 
              startDate: data.startDate, 
              endDate: data.endDate 
            }
          });
        }}
      />
    </Form.Group>
  );

  return (
    <div>
      {state.tasks.map(task => (
        <TaskRow data={task} key={task.id} />
      ))}
    </div>
  );
}
解决方法

焦点丢失的核心原因是**TaskRow组件被定义在TenderCreate组件内部**,每次TenderCreate重渲染时,TaskRow都会被重新创建为全新的组件类型,React会卸载旧组件并挂载新组件,直接导致焦点丢失。这和reducer更新数组的方式无关。

具体解决步骤:

  • 将TaskRow组件移到TenderCreate组件外部,避免父组件重渲染时反复创建新组件
  • 保持reducer中正确的不可变更新逻辑(你的原reducer逻辑没问题,可额外优化对象更新的不可变性)

修改后的完整代码:

import { useReducer } from 'react';
import { Form, Row } from 'react-bootstrap';

// 把TaskRow移到父组件外部,避免每次重渲染被重新创建
const TaskRow = ({ data, dispatch }) => (
  <Form.Group as={Row} controlId={`task-${data.id}`} className="mb-3">
    <Form.Control
      type="text"
      placeholder=""
      name="task"
      className={data.errorTask ? 'is-invalid' : ''}
      value={data.task}
      onChange={e => {
        dispatch({
          type: 'taskUpdate',
          payload: { 
            id: data.id, 
            task: e.target.value, 
            startDate: data.startDate, 
            endDate: data.endDate 
          }
        });
      }}
    />
  </Form.Group>
);

const reducer = (state, action) => {
  switch (action.type) {
    case 'taskUpdate': {
      const index = state.tasks.findIndex(task => task.id === action.payload.id);
      const newArray = [...state.tasks];
      
      // 优化:采用不可变方式更新对象,符合React状态更新规范
      newArray[index] = {
        ...newArray[index],
        task: action.payload.task,
        startDate: action.payload.startDate,
        endDate: action.payload.endDate
      };

      return { 
        ...state, 
        tasks: newArray, 
       };
    }
    default:
      throw new Error();
  }
};

const TenderCreate = () =>  {
  const [state, dispatch] = useReducer(reducer, {
    tasks: [{ 
      id: 1,
      task: '', 
      errorTask:'',
      startDate: '', 
      errorStartDateTime:'',
      endDate:'',
      errorEndDateTime: ''
    }]
  });

  return (
    <div>
      {state.tasks.map(task => (
        <TaskRow 
          key={task.id} 
          data={task} 
          dispatch={dispatch} 
        />
      ))}
    </div>
  );
}

export default TenderCreate;

额外优化说明:

  • 给Form.Group的controlId设置动态值(task-${data.id}),避免多个行使用相同ID,符合表单组件规范
  • 用...newArray[index]的方式更新对象,保持状态不可变性,避免潜在的状态更新问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 22:00:57