如何在MERN应用的登录表单中展示后端返回的错误信息
登录时无法获取并展示后端返回的错误信息解决方案
当前React+Redux+Node.js/Express/MongoDB的登录流程中,用户使用未注册账号或错误密码登录时,后端会返回错误信息,但前端无法获取并展示。以下是具体修改方案:
1. 更新Redux的authReducer,添加错误状态管理
在Redux状态中新增登录错误字段,以及对应的状态更新action,用于存储和清除登录错误信息:
import { createSlice } from '@reduxjs/toolkit'; const initialState = { user: null, token: null, mode: "dark", userId: "63701cc1f03239c72c000180", loginError: null, // 新增登录错误存储字段 } export const authSlice = createSlice({ name: "auth", initialState, reducers: { setLogin: (state, action) => { state.user = action.payload.user; state.token = action.payload.token; state.loginError = null; // 登录成功时自动清除错误 }, setLogout: (state) => { state.user = null; state.token = null; state.loginError = null; }, setMode: (state) => { state.mode = state.mode === 'light' ? "dark" : 'light'; }, setLoginError: (state, action) => { // 新增设置登录错误的action state.loginError = action.payload; }, clearLoginError: (state) => { // 新增清除登录错误的action state.loginError = null; }, } }) export const { setLogin, setLogout, setMode, setLoginError, clearLoginError } = authSlice.actions; export default authSlice.reducer;
2. 修改前端Form组件,处理错误响应并展示
修改登录函数以处理后端错误响应,同时在表单中添加错误展示区域:
关键修改点:
- 导入
useSelector获取Redux中的错误信息 - 登录前清除旧错误,非2xx响应时捕获错误并存入Redux
- 在表单中添加错误提示区域
import React, { useState } from 'react'; import { Box, Button, TextField, useMediaQuery, Typography, useTheme } from '@mui/material'; import { EditOutlined } from '@mui/icons-material'; import { Formik } from 'formik'; import * as yup from "yup"; import { useNavigate } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import { setLogin, setLoginError, clearLoginError } from 'state/authReducer'; import Dropzone from 'react-dropzone'; import FlexBetween from 'components/FlexBetween'; // 以下Schema和初始值代码保持不变 const registerSchema = yup.object().shape({ name: yup.string().required("Обязательное поле"), email: yup.string().email("Почта введена неправильно").required("Обязательное поле"), password: yup.string().required("Обязательное поле"), occupation: yup.string().required("Укажите вашу должность"), picture: yup.string().required("Выберите изображение для вашего профиля") }) const loginSchema = yup.object().shape({ email: yup.string().email("Почта введена неправильно").required("Обязательное поле"), password: yup.string().required("Обязательное поле"), }) const initialValuesRegister = { name: "", email: "", password: "", occupation: "" } const initialValuesLogin = { email: "", password: "", } const Form = () => { const [pageType, setPageType] = useState("login"); const { palette } = useTheme(); const dispatch = useDispatch(); const navigate = useNavigate(); const isNonMobile = useMediaQuery("(min-width: 600px)"); const isLogin = pageType === "login"; const isRegister = pageType === "register"; const loginError = useSelector(state => state.auth.loginError); // 获取Redux中的登录错误 // register函数保持不变 const register = async (values, onSubmitProps) => { const formData = new FormData(); for (let value in values) { formData.append(value, values[value]) } formData.append("picturePath", values.picture.name); const savedUserResponse = await fetch( "http://localhost:5001/auth/register", { method: "POST", body: formData, } ); const savedUser = await savedUserResponse.json(); onSubmitProps.resetForm(); if (savedUser) { setPageType("login"); } } // 修改后的login函数,处理错误响应 const login = async (values, onSubmitProps) => { dispatch(clearLoginError()); // 发起登录前清除旧错误 try { const loggedInResponse = await fetch( "http://localhost:5001/auth/login", { method: "POST", headers: { "Content-Type": "application/json"}, body: JSON.stringify(values), } ); // 检查响应状态,非2xx状态码视为错误 if (!loggedInResponse.ok) { const errorData = await loggedInResponse.json(); throw new Error(errorData.msg || '登录失败'); } const loggedIn = await loggedInResponse.json(); onSubmitProps.resetForm(); dispatch( setLogin({ user: loggedIn.user, token: loggedIn.token, }) ); navigate("/dashboard"); } catch (err) { dispatch(setLoginError(err.message)); // 将错误信息存入Redux } }; const handleFormSubmit = async(values, onSubmitProps) => { if (isLogin) { await login(values, onSubmitProps); } if (isRegister) { await register(values, onSubmitProps); } }; return ( <Formik onSubmit={handleFormSubmit} initialValues={isLogin ? initialValuesLogin : initialValuesRegister} validationSchema={isLogin ? loginSchema : registerSchema} > {({ values, errors, touched, handleBlur, handleChange, handleSubmit, setFieldValue, resetForm, }) => ( <form onSubmit={handleSubmit}> {/* 表单输入区域代码保持不变 */} <Box display="grid" gap="10px" gridTemplateColumns="repeat(4, minmax(0, 1fr))" sx={{ "& > div": { gridColumn: isNonMobile ? undefined : "span 4" }, }} > {isRegister && ( <> <TextField label="ФИО" onBlur={handleBlur} onChange={handleChange} value={values.name} name="name" error={Boolean(touched.name) && Boolean(errors.name)} helperText={touched.name && errors.name} sx={{ gridColumn: "span 4" }} /> <TextField label="Должность" onBlur={handleBlur} onChange={handleChange} value={values.occupation} name="occupation" error={Boolean(touched.occupation) && Boolean(errors.occupation)} helperText={touched.occupation && errors.occupation} sx={{ gridColumn: "span 4" }} /> <Box gridColumn="span 4" border={`1px solid ${palette.divider}`} borderRadius="5px" p="1rem" > <Dropzone acceptedFiles=".jpg,.jpeg,.png" multiple={false} onDrop={(acceptedFiles) => { setFieldValue("picture", acceptedFiles[0]) }} > {({getRootProps, getInputProps}) => ( <Box {...getRootProps()} border={`2px dashed ${palette.primary.main}`} p="1rem" sx={{ "&:hover": { cursor: "pointer" }}} > <input {...getInputProps()} /> {!values.picture ? ( <p>Add Picture Here</p> ) : ( <FlexBetween> <Typography>{values.picture.name}</Typography> <EditOutlined /> </FlexBetween> )} </Box> )} </Dropzone> </Box> </> )} <TextField autoComplete='new-password' placeholder="Логин" onBlur={handleBlur} onChange={handleChange} value={values.email} name="email" error={Boolean(touched.email) && Boolean(errors.email)} helperText={touched.email && errors.email} sx={{ gridColumn: "span 4" }} /> <TextField autoComplete='new-password' placeholder="Пароль" type="password" onBlur={handleBlur} onChange={handleChange} value={values.password} name="password" error={Boolean(touched.password) && Boolean(errors.password)} helperText={touched.password && errors.password} sx={{ gridColumn: "span 4" }} /> </Box> {/* 新增错误展示区域 */} {loginError && ( <Typography color="error" sx={{ mb: 1, textAlign: 'center' }}> {loginError} </Typography> )} {/* 按钮区域代码保持不变 */} <Box> <Button fullWidth type="submit" sx={{ m: "1rem 0", p: "1rem", backgroundColor: palette.text.primary, color: palette.background.alt, "&:hover": {backgroundColor: palette.text.primary, boxShadow: "0 4px 7px rgba(0, 0, 0, 0.15), 0 100px 80px rgba(255, 255, 255, 0.02), 0 42px 33px rgba(255, 255, 255, 0.024), 0 22px 18px rgba(255, 255, 255, 0.028), 0 12px 10px rgba(255, 255, 255, 0.034), 0 7px 5px rgba(255, 255, 255, 0.04), 0 3px 2px rgba(255, 255, 255, 0.07)"} }} > {isLogin ? "ВОЙТИ" : "ЗАРЕГЕСТРИРОВАТЬСЯ"} </Button> <Typography onClick={() => { setPageType(isLogin ? "register" : "login"); resetForm(); }} sx={{ textDecoration: "underline", color: palette.text.disabled, "&:hover": { cursor: "pointer", color: palette.primary.main }, }} > {isLogin ? "Нет аккаунта? Создайте его здесь." : "Уже есть аккаунт? Войдите здесь."} </Typography> </Box> </form> )} </Formik> ) } export default Form;
3. 后端代码无需修改
当前后端的login函数已经正确返回错误状态码和对应信息,无需调整。
内容的提问来源于stack exchange,提问作者Богдан Шевченко
相关产品推荐
相关产品推荐

