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

Redux Store对象数组更新引发输入框失焦问题求助

输入框失焦问题:React+Redux管理数组状态时的重渲染解决方案

问题描述

在React项目中使用Redux管理董事信息的对象数组状态,修改输入框内容时Redux状态能成功更新,但组件会触发重渲染,导致输入框失去焦点,无法流畅输入。尝试过直接修改状态、使用filter替代map的方式,均未解决问题。

组件代码

import { Box, Button, Stack, TextField, Typography } from "@mui/material";
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import {
  handleDirectorAdd,
  handleDirectorInfoFormChange,
  handleDirectorRemove,
} from "../../../../redux/slices/directorInfo";

const DirectorsInformation = () => {
  const dispatch = useDispatch();
  const directorsInfo = useSelector((state) => state.directorInfo);

  const handleChange = (e, index) => {
    const { name, value } = e.target;
    if (name && value) {
      dispatch(handleDirectorInfoFormChange({ name, value, index }));
    }
  };

  const handleAdd = () => {
    dispatch(handleDirectorAdd());
  };

  const handleRemove = (e, index) => {
    dispatch(handleDirectorRemove({ index }));
  };

  return (
    <form
      onSubmit={handleSubmit}
      noValidate
      style={{
        padding: "1rem",
        backgroundColor: "white",
        border: "1px solid darkgrey",
        textAlign: "center",
      }}
    >
      <Typography variant="body1">
        <strong>Director's Information</strong>
      </Typography>
      <hr />
      {directorsInfo?.map((directorInfo, index) => (
        <div key={uuidv4()}>
          <Stack
            spacing={1}
            alignItems="center"
            style={{
              padding: "1rem",
              backgroundColor: "rgb(227, 227, 227)",
            }}
          >
            <Typography variant="body1">Director's Information</Typography>
            <Button
              variant="contained"
              color="error"
              sx={{ textTransform: "none" }}
              onClick={(e) => handleRemove(e, index)}
            >
              Remove
            </Button>
            <Typography variant="body1">
              <strong>Full Name</strong>
            </Typography>
            <TextField
              fullWidth
              size="small"
              label="Full Name"
              name="name"
              value={directorInfo.name}
              onChange={(e) => handleChange(e, index)}
              sx={{ backgroundColor: "white" }}
            />
            <Typography variant="body1">
              <strong>Official Email</strong>
            </Typography>
            <TextField
              fullWidth
              size="small"
              label="Official Email"
              name="email"
              type="email"
              value={directorInfo.email}
              onChange={(e) => handleChange(e, index)}
              sx={{ backgroundColor: "white" }}
            />
            <Typography variant="body1">
              <strong>Contact Number</strong>
            </Typography>
            <TextField
              fullWidth
              size="small"
              label="Contact Number"
              name="contactNumber"
              value={directorInfo.contactNumber}
              onChange={(e) => handleChange(e, index)}
              sx={{ backgroundColor: "white" }}
            />
            <Typography variant="body1">
              <strong>Title</strong>
            </Typography>
            <TextField
              fullWidth
              size="small"
              label="Tile"
              name="title"
              type="string"
              value={directorInfo.title}
              onChange={(e) => handleChange(e, index)}
              sx={{ backgroundColor: "white" }}
            />
          </Stack>
          <Button
            variant="contained"
            color="success"
            sx={{ textTransform: "none", margin: "2rem 0" }}
            onClick={handleAdd}
          >
            Add Directors Information
          </Button>
        </div>
      ))}
    </form>
  );
};

export default DirectorsInformation;

Reducer及Action代码

import { createSlice, current } from "@reduxjs/toolkit";
const initialValues = {
  name: "",
  email: "",
  contactNumber: "",
  username: "",
  title: "",
};
const initialState = [initialValues];

const directorInfo = createSlice({
  name: "directorInfo",
  initialState,
  reducers: {
    handleDirectorAdd(state, action) {
      return [...state, initialValues];
    },
    handleDirectorRemove(state, action) {
      return state.filter((_, index) => index !== action.payload.index);
    },
    handleDirectorInfoFormChange(state, action) {
      const { index, name, value } = action.payload;
      return state.map((director, idx) => {
          if (idx === index) {
          return {
            ...director,
            [name]: value,
          };
        } else {
          return director;
        }
      });
    },
  },
});

export const {
  handleDirectorAdd,
  handleDirectorRemove,
  handleDirectorInfoFormChange,
} = directorInfo.actions;

export default directorInfo.reducer;

解决方案

1. 修复列表项的key值(核心问题)

输入框失焦的根本原因是列表渲染时使用了动态生成的uuidv4()作为key。每次组件重渲染,uuidv4()都会生成新的字符串,React会将其识别为全新的组件,卸载旧的输入框并重新挂载新的,导致焦点丢失。

修改步骤:

  • 更新initialValues,添加唯一id字段:
    import { v4 as uuidv4 } from "uuid";
    const initialValues = {
      id: uuidv4(), // 添加唯一id
      name: "",
      email: "",
      contactNumber: "",
      username: "",
      title: "",
    };
    
  • 修改handleDirectorAdd动作,每次添加新董事时生成新id:
    handleDirectorAdd(state, action) {
      return [...state, {...initialValues, id: uuidv4()}]; // 生成新的唯一id
    },
    
  • 组件中使用director的id作为key:
    {directorsInfo?.map((directorInfo, index) => (
      <div key={directorInfo.id}> {/* 替换uuidv4()为directorInfo.id */}
        // ... 其余内容不变
      </div>
    ))}
    

2. 优化useSelector的重渲染触发逻辑

为了避免不必要的组件重渲染,可以使用shallowEqual来对比状态数组,只有当数组或内部元素的引用发生变化时才更新组件:

import { useDispatch, useSelector, shallowEqual } from "react-redux";

// ...

const directorsInfo = useSelector((state) => state.directorInfo, shallowEqual);

3. 修复小错误

组件中TextField的label写成了"Tile",应该修正为"Title":

<TextField
  fullWidth
  size="small"
  label="Title" // 修正拼写错误
  name="title"
  type="string"
  value={directorInfo.title}
  onChange={(e) => handleChange(e, index)}
  sx={{ backgroundColor: "white" }}
/>

说明

通过给每个董事对象分配固定的唯一id作为列表项key,React能够正确识别列表项的身份,重渲染时只会更新对应项的内容,而不会卸载并重新挂载组件,输入框的焦点就能保持稳定。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 12:47:08