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

React组件状态变更时输入框失焦问题(MUI受控表单)

受控表单输入框状态变更时失去焦点的问题

我基于MUI开发的受控表单此前运行完全正常,仅修改了部分状态变量名称后,状态变更时输入框突然失去焦点。我认为变量名修改并非问题根源,但不清楚该问题的成因,相关代码如下:

import { useEffect, useState } from "react";
import Axios from "axios";
import { Schedule } from "./Schedule";
import { PageHeader } from "./PageHeader";
import { Button, TextField, styled, CircularProgress } from "@mui/material";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import SendIcon from "@mui/icons-material/Send";

function Form() {
  const [formData, setFormData] = useState({ startTime: "", endTime: "" });
  const [file, setFile] = useState("");
  const [data, setData] = useState("");
  const [newValue, setValue] = useState(null);
  const [loading, setLoading] = useState(false);
  console.log(formData);
  console.log("Rerendered");

  function handleChange(event) {
    setFormData((previousFormData) => {
      return {
        ...previousFormData,
        [event.target.name]: event.target.value,
      };
    });
  }

  function handleClick(event) {
    setLoading(true);
    event.preventDefault();
    setTimeout(() => {
      const postFormData = new FormData();
      postFormData.append("startTime", formData.startTime);
      postFormData.append("endTime", formData.endTime);
      postFormData.append("date", newValue);
      postFormData.append("file", file);

      Axios.post("http://localhost:3001/aip", postFormData, {
        headers: {
          "Access-Control-Allow-Origin": "cors",
        },
      })
        .then((res) => setData(res.data))
        .then(setLoading(false))
        .catch((err) => console.log(err));
    }, 2000);
  }

  const Container = styled("main")({
    marginTop: 50,
    display: "grid",
    justifyItems: "center",
  });
  const Div = styled("div")({
    display: "flex",
    gap: 50,
  });
  const Form = styled("form")({
    border: "none",
    width: "100%",
    height: "100%",
    margin: "auto",
    display: "grid",
    justifyItems: "center",
    gap: 50,
  });

  return (
    <>
      <PageHeader />
      <Container>
        <Form onSubmit={handleClick} encType="multipart/form-data">
          <Div class="timeInput">
            <TextField
              label="From"
              name="startTime"
              variant="outlined"
              placeholder="9:00"
              onChange={handleChange}
              value={formData.startTime}
              required
            />
            <TextField
              label="To"
              name="endTime"
              variant="outlined"
              placeholder="9:30"
              onChange={handleChange}
              value={formData.endTime}
              required
            />
          </Div>
          <div class="dateInput">
            <LocalizationProvider dateAdapter={AdapterDayjs}>
              <DesktopDatePicker
                value={newValue}
                label="Choose Date"
                onChange={(newValue) => setValue(newValue)}
                renderInput={(params) => <TextField {...params} />}
              />
            </LocalizationProvider>
          </div>
          <Div>
            <Button sx={{ gap: 2 }} variant="contained" component="label">
              Upload Schedule
              <input
                hidden
                accept=".xlsx"
                multiple
                type="file"
                onChange={(event) => {
                  const file = event.target.files[0];
                  setFile(file);
                }}
              />
              <FileUploadIcon />
            </Button>
            <Button sx={{ gap: 2 }} variant="contained" component="label">
              Search
              <input hidden type="submit" />
              <SendIcon />
            </Button>
          </Div>
        </Form>
      </Container>
      {loading ? (
        <CircularProgress sx={{ alignItems: "center" }} />
      ) : (
        <Schedule data={data} />
      )}
    </>
  );
}

export default Form;

问题根源

输入框失焦的核心原因是你在Form组件内部定义了MUI的styled组件。每次Form组件因状态变更重新渲染时,Container、Div、Form这些styled组件都会被重新创建为全新的组件类型。React会判定这些是完全不同的元素,卸载旧组件并重新挂载新组件,直接导致输入框失去焦点。

变量名修改只是触发组件重新渲染的契机,并非问题本质。

解决方法

将所有通过styled创建的组件移到Form组件外部,避免每次渲染都重复创建:

import { useEffect, useState } from "react";
import Axios from "axios";
import { Schedule } from "./Schedule";
import { PageHeader } from "./PageHeader";
import { Button, TextField, styled, CircularProgress } from "@mui/material";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import SendIcon from "@mui/icons-material/Send";

// 将styled组件移到组件外部,确保只创建一次
const Container = styled("main")({
  marginTop: 50,
  display: "grid",
  justifyItems: "center",
});
const Div = styled("div")({
  display: "flex",
  gap: 50,
});
const StyledForm = styled("form")({
  border: "none",
  width: "100%",
  height: "100%",
  margin: "auto",
  display: "grid",
  justifyItems: "center",
  gap: 50,
});

function Form() {
  const [formData, setFormData] = useState({ startTime: "", endTime: "" });
  const [file, setFile] = useState("");
  const [data, setData] = useState("");
  const [newValue, setValue] = useState(null);
  const [loading, setLoading] = useState(false);
  console.log(formData);
  console.log("Rerendered");

  function handleChange(event) {
    setFormData((previousFormData) => {
      return {
        ...previousFormData,
        [event.target.name]: event.target.value,
      };
    });
  }

  function handleClick(event) {
    setLoading(true);
    event.preventDefault();
    setTimeout(() => {
      const postFormData = new FormData();
      postFormData.append("startTime", formData.startTime);
      postFormData.append("endTime", formData.endTime);
      postFormData.append("date", newValue);
      postFormData.append("file", file);

      Axios.post("http://localhost:3001/aip", postFormData, {
        headers: {
          "Access-Control-Allow-Origin": "cors",
        },
      })
        .then((res) => setData(res.data))
        .then(() => setLoading(false)) // 修正:用箭头函数包裹,确保请求完成后再执行
        .catch((err) => console.log(err));
    }, 2000);
  }

  return (
    <>
      <PageHeader />
      <Container>
        <StyledForm onSubmit={handleClick} encType="multipart/form-data">
          <Div className="timeInput"> {/* 规范写法:React中用className代替class */}
            <TextField
              label="From"
              name="startTime"
              variant="outlined"
              placeholder="9:00"
              onChange={handleChange}
              value={formData.startTime}
              required
            />
            <TextField
              label="To"
              name="endTime"
              variant="outlined"
              placeholder="9:30"
              onChange={handleChange}
              value={formData.endTime}
              required
            />
          </Div>
          <div className="dateInput"> {/* 替换class为className */}
            <LocalizationProvider dateAdapter={AdapterDayjs}>
              <DesktopDatePicker
                value={newValue}
                label="Choose Date"
                onChange={(newValue) => setValue(newValue)}
                renderInput={(params) => <TextField {...params} />}
              />
            </LocalizationProvider>
          </div>
          <Div>
            <Button sx={{ gap: 2 }} variant="contained" component="label">
              Upload Schedule
              <input
                hidden
                accept=".xlsx"
                multiple
                type="file"
                onChange={(event) => {
                  const file = event.target.files[0];
                  setFile(file);
                }}
              />
              <FileUploadIcon />
            </Button>
            <Button sx={{ gap: 2 }} variant="contained" component="label">
              Search
              <input hidden type="submit" />
              <SendIcon />
            </Button>
          </Div>
        </StyledForm>
      </Container>
      {loading ? (
        <CircularProgress sx={{ alignItems: "center" }} />
      ) : (
        <Schedule data={data} />
      )}
    </>
  );
}

export default Form;

额外修正点

  • 原代码中then(setLoading(false))会在请求完成前立即执行,导致loading状态提前关闭,需改为then(() => setLoading(false))。
  • React中元素的类名属性需用className而非class,虽然部分场景下class能生效,但className是符合React规范的写法。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 23:05:24