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

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
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 23:56:48