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

如何在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,提问作者Богдан Шевченко

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 23:32:00