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
相关产品推荐
相关产品推荐

