React登录页输入框输入字符后失焦问题求助
React登录页输入框失焦问题解决
问题描述
我正在学习React,使用MUI、Formik和Yup搭建了包含邮箱与密码输入框的登录页面。但输入任意字符后,输入框就会失去焦点,其余功能均正常。将代码移至App.js时无此问题,但放回Login.jsx后问题重现。
相关代码
app.js
import { Navigate, RouterProvider, createBrowserRouter, } from "react-router-dom"; import { ColorModeContext, useMode } from "./theme"; import Login from "./pages/login/Login"; import { useContext, useState } from "react"; import { AuthContext } from "./context/AuthContext"; import { Box, CssBaseline, ThemeProvider } from "@mui/material"; import Home from "./pages/home/Home"; import ErrorPage from "./pages/error/Error"; import SidebarComponent from "./components/sidebar/Sidebar"; import Register from "./pages/register/Register"; function App() { const currentUser = useContext(AuthContext); const [theme, colorMode] = useMode(); const Layout = () => { <Box sx={{ display: "flex", height: "100%", minHeight: "400px", }} > <SidebarComponent /> <main>rtest</main> </Box>; }; const ProtectedRoute = ({ children }) => { if (!currentUser.user) { return <Navigate to="/login" />; } return children; }; const router = createBrowserRouter([ { path: "/", element: ( <ProtectedRoute> <Layout /> </ProtectedRoute> ), children: [ { path: "/", element: <Home />, }, ], errorElement: <ErrorPage />, }, { path: "/login", element: <Login />, }, { path: "/login", element: <Register />, }, ]); return ( <ColorModeContext.Provider value={colorMode}> <ThemeProvider theme={theme}> <CssBaseline /> <div className="App"> <RouterProvider router={router} /> </div> </ThemeProvider> </ColorModeContext.Provider> ); } export default App;
Login.jsx
import Logo from "../../assets/backticklogo.svg"; import { Link, useNavigate } from "react-router-dom"; import { useContext, useEffect, useState } from "react"; import { AuthContext } from "../../context/AuthContext"; import apiClient from "../../services/api"; import { Box, Button, Stack, TextField, Typography } from "@mui/material"; import styled from "@emotion/styled"; import { useFormik } from "formik"; import * as yup from "yup"; const validationSchema = yup.object({ email: yup .string("Enter your email") .email("Enter a valid email") .required("Email is required"), password: yup .string("Enter your password") .min(8, "Password should be of minimum 8 characters length") .required("Password is required"), }); const Login = () => { const localplainTextToken = localStorage.getItem("plainTextToken") || null; const [isAuth, setIsAuth] = useState(localStorage.getItem("isAuth") || null); const [plainTextToken, setplainTextToken] = useState(localplainTextToken); const remember = true; const navigate = useNavigate(); const { user, dispatch } = useContext(AuthContext); useEffect(() => { isAuth && navigate("/"); }, [isAuth]); const handleUpdateUser = (userInfo) => { console.log(userInfo); dispatch({ type: "LOGIN", payload: userInfo }); localStorage.setItem("user", JSON.stringify(userInfo)); localStorage.setItem("isAuth", true); setIsAuth(true); }; const formik = useFormik({ initialValues: { email: "", password: "", }, validationSchema: validationSchema, onSubmit: async (values) => { // alert(JSON.stringify(values, null, 2)); try { await apiClient .post("login", { email: values.email, password: values.password, }) .then((res) => { console.log(res); localStorage.setItem("plainTextToken", res.data.plainTextToken); setplainTextToken(res.data.plainTextToken); return res.data; }) .then(async (data) => { try { let user = await apiClient.get(`user/${data.user_id}`, { headers: { Authorization: `Bearer ${data.plainTextToken}`, Accept: "application/json", }, }); console.log(user); handleUpdateUser(user.data); // navigate("/"); } catch (error) {} }); } catch (error) { console.log(error); } }, }); /***********Styled component */ const LoginContainer = styled(Box)(({ theme }) => ({ display: "flex", flex: "1 1 auto", height: "100%", width: "100%", [theme.breakpoints.up("xs")]: { flexDirection: "column-reverse", }, [theme.breakpoints.up("md")]: { flexDirection: "row", }, })); const RightBox = styled(Box)(({ theme }) => ({ backgroundColor: "rgb(14, 8, 39)", display: "flex", flexDirection: "column", maxWidth: "100%", [theme.breakpoints.up("xs")]: { flex: "1 1 auto", padding: "32px", }, [theme.breakpoints.up("md")]: { flex: "0 0 auto", justifyContent: "center", padding: "64px", width: "600px", }, })); const LeftBox = styled(Box)(({ theme }) => ({ display: "flex", alignItems: "center", backgroundColor: "rgb(28, 37, 54)", backgroundImage: "url(/assets/gradient-bg.svg)", backgroundPosition: "center top", backgroundRepeat: "no-repeat", color: "rgb(255, 255, 255)", justifyContent: "center", [theme.breakpoints.up("xs")]: { flex: "0 0 auto", padding: "32px", }, [theme.breakpoints.up("md")]: { flex: "1 1 auto", padding: "64px", }, })); return ( <LoginContainer> <LeftBox> <Box sx={{ maxWidth: "900px" }}> <Typography variant="h4"> Welcome to Backtic</Typography> <Typography sx={{ color: "rgb(108, 115, 127)" }}> Some cool text here to represent out Backtick </Typography> </Box> </LeftBox> <RightBox> <div> <Box mb={"32px"}> <Link to="/"> <img src={Logo} alt="Backtick Logo" width={140} /> </Link> </Box> </div> <div> <Stack direction="column" mb={"32px"}> <Typography variant="h4" sx={{ color: "rgb(108, 115, 127)" }}> Login </Typography> <Typography sx={{ color: "rgb(108, 115, 127)" }}> Don't have an account? <Link to="/register">Register</Link> </Typography> </Stack> <form onSubmit={formik.handleSubmit}> <Stack direction="column"> <TextField fullWidth id="email" name="email" label="Email" value={formik.values.email} onChange={formik.handleChange} onBlur={formik.handleBlur} error={formik.touched.email && Boolean(formik.errors.email)} helperText={formik.touched.email && formik.errors.email} /> <TextField fullWidth id="password" name="password" label="Password" type="password" value={formik.values.password} onChange={formik.handleChange} onBlur={formik.handleBlur} error={ formik.touched.password && Boolean(formik.errors.password) } helperText={formik.touched.password && formik.errors.password} sx={{ marginTop: "24px" }} /> </Stack> <Button color="primary" variant="contained" fullWidth type="submit" size="large" sx={{ marginTop: "24px" }} > Submit </Button> </form> </div> </RightBox> </LoginContainer> ); }; export default Login;
问题原因
你将styled-components的定义放在了Login组件内部,每次组件因输入框值变化触发重新渲染时,这些LoginContainer、RightBox、LeftBox都会被重新创建。React会识别为全新的组件类型,销毁旧DOM节点并重新挂载,直接导致输入框失去焦点。
移至App.js时问题消失,是因为App组件的渲染频率远低于Login组件,重新创建styled组件的次数极少,不会触发明显的失焦问题。
解决方案
将所有styled组件的定义移到Login组件外部,确保它们只被创建一次:
import Logo from "../../assets/backticklogo.svg"; import { Link, useNavigate } from "react-router-dom"; import { useContext, useEffect, useState } from "react"; import { AuthContext } from "../../context/AuthContext"; import apiClient from "../../services/api"; import { Box, Button, Stack, TextField, Typography } from "@mui/material"; import styled from "@emotion/styled"; import { useFormik } from "formik"; import * as yup from "yup"; // --- 将styled组件移到组件外部 --- const LoginContainer = styled(Box)(({ theme }) => ({ display: "flex", flex: "1 1 auto", height: "100%", width: "100%", [theme.breakpoints.up("xs")]: { flexDirection: "column-reverse", }, [theme.breakpoints.up("md")]: { flexDirection: "row", }, })); const RightBox = styled(Box)(({ theme }) => ({ backgroundColor: "rgb(14, 8, 39)", display: "flex", flexDirection: "column", maxWidth: "100%", [theme.breakpoints.up("xs")]: { flex: "1 1 auto", padding: "32px", }, [theme.breakpoints.up("md")]: { flex: "0 0 auto", justifyContent: "center", padding: "64px", width: "600px", }, })); const LeftBox = styled(Box)(({ theme }) => ({ display: "flex", alignItems: "center", backgroundColor: "rgb(28, 37, 54)", backgroundImage: "url(/assets/gradient-bg.svg)", backgroundPosition: "center top", backgroundRepeat: "no-repeat", color: "rgb(255, 255, 255)", justifyContent: "center", [theme.breakpoints.up("xs")]: { flex: "0 0 auto", padding: "32px", }, [theme.breakpoints.up("md")]: { flex: "1 1 auto", padding: "64px", }, })); // --- styled组件定义结束 --- const validationSchema = yup.object({ email: yup .string("Enter your email") .email("Enter a valid email") .required("Email is required"), password: yup .string("Enter your password") .min(8, "Password should be of minimum 8 characters length") .required("Password is required"), }); const Login = () => { const localplainTextToken = localStorage.getItem("plainTextToken") || null; const [isAuth, setIsAuth] = useState(localStorage.getItem("isAuth") || null); const [plainTextToken, setplainTextToken] = useState(localplainTextToken); const remember = true; const navigate = useNavigate(); const { user, dispatch } = useContext(AuthContext); useEffect(() => { isAuth && navigate("/"); }, [isAuth]); const handleUpdateUser = (userInfo) => { console.log(userInfo); dispatch({ type: "LOGIN", payload: userInfo }); localStorage.setItem("user", JSON.stringify(userInfo)); localStorage.setItem("isAuth", true); setIsAuth(true); }; const formik = useFormik({ initialValues: { email: "", password: "", }, validationSchema: validationSchema, onSubmit: async (values) => { // alert(JSON.stringify(values, null, 2)); try { await apiClient .post("login", { email: values.email, password: values.password, }) .then((res) => { console.log(res); localStorage.setItem("plainTextToken", res.data.plainTextToken); setplainTextToken(res.data.plainTextToken); return res.data; }) .then(async (data) => { try { let user = await apiClient.get(`user/${data.user_id}`, { headers: { Authorization: `Bearer ${data.plainTextToken}`, Accept: "application/json", }, }); console.log(user); handleUpdateUser(user.data); // navigate("/"); } catch (error) {} }); } catch (error) { console.log(error); } }, }); return ( <LoginContainer> <LeftBox> <Box sx={{ maxWidth: "900px" }}> <Typography variant="h4"> Welcome to Backtic</Typography> <Typography sx={{ color: "rgb(108, 115, 127)" }}> Some cool text here to represent out Backtick </Typography> </Box> </LeftBox> <RightBox> <div> <Box mb={"32px"}> <Link to="/"> <img src={Logo} alt="Backtick Logo" width={140} /> </Link> </Box> </div> <div> <Stack direction="column" mb={"32px"}> <Typography variant="h4" sx={{ color: "rgb(108, 115, 127)" }}> Login </Typography> <Typography sx={{ color: "rgb(108, 115, 127)" }}> Don't have an account? <Link to="/register">Register</Link> </Typography> </Stack> <form onSubmit={formik.handleSubmit}> <Stack direction="column"> <TextField fullWidth id="email" name="email" label="Email" value={formik.values.email} onChange={formik.handleChange} onBlur={formik.handleBlur} error={formik.touched.email && Boolean(formik.errors.email)} helperText={formik.touched.email && formik.errors.email} /> <TextField fullWidth id="password" name="password" label="Password" type="password" value={formik.values.password} onChange={formik.handleChange} onBlur={formik.handleBlur} error={ formik.touched.password && Boolean(formik.errors.password) } helperText={formik.touched.password && formik.errors.password} sx={{ marginTop: "24px" }} /> </Stack> <Button color="primary" variant="contained" fullWidth type="submit" size="large" sx={{ marginTop: "24px
相关产品推荐
相关产品推荐

