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

如何在表单验证失败时保持Material UI Modal不关闭?

问题

使用Material UI Modal搭建注册表单,期望实现:必填字段为空时输入框变红或弹出toast提示;但当前存在以下问题:

  • 表单添加noValidate属性后,空字段会变红,但Modal仍会关闭
  • 字段填写完整后,若出现密码不匹配、后端返回邮箱已存在等错误,Modal直接关闭且无错误提示
  • 几乎注释所有代码仅尝试输出错误日志,Modal依然会关闭

需要解决:存在错误时保持Modal处于打开状态。

注册表单代码

import React, { useEffect, useRef, useState } from 'react';
import DotLoader from 'react-spinners/DotLoader';
import { useDispatch, useSelector } from 'react-redux';
//import { signUp } from '../../actions/AuthAction';
import axios from 'axios';
import {
  Button,
  Container,
  FormControl,
  Grid,
  IconButton,
  InputAdornment,
  InputLabel,
  OutlinedInput,
  Paper,
  TextField,
  Typography,
} from '@mui/material';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import { toast } from 'react-toastify';

const RegisterForm = () => {
  const [showPassword, setShowPassword] = React.useState(false);
  const handleClickShowPassword = () => setShowPassword((show) => !show);

  const handleMouseDownPassword = (
    event: React.MouseEvent<HTMLButtonElement>
  ) => {
    event.preventDefault();
  };

  const dispatch = useDispatch();
  //const loading = useSelector((state) => state.authReducer.loading);
  const userInfo = {
    first_name: '',
    last_name: '',
    email: '',
    password: '',
    confirmPassword: '',
  };

  const [user, setUser] = useState(userInfo);
  const { first_name, last_name, email, password, confirmPassword } = user;

  const [firstNameError, setFirstNameError] = useState(false);
  const [lastNameError, setLastNameError] = useState(false);
  const [emailError, setEmailError] = useState(false);
  const [passwordError, setPasswordError] = useState(false);
  const [confirmPasswordError, setConfirmPasswordError] = useState(false);

  const [error, setError] = useState('');
  const [success, setSuccess] = useState('');
  const [loading, setLoading] = useState(false);

  const handleRegisterChange = (e) => {
    const { name, value } = e.target;
    setUser({ ...user, [name]: value });
  };

  const registerSubmit = async () => {
    setFirstNameError(false);
    setLastNameError(false);
    setEmailError(false);
    setPasswordError(false);
    setConfirmPasswordError(false);

    dispatch({ type: 'AUTH_REQUEST' });
    try {
      if (user.first_name === '') {
        setFirstNameError(true);
      }
      if (user.password !== user.confirmPassword) {
        toast.error('Passwords do not match.');
      }

      // const { data } = await axios.post('/api/users/register', {
      //   first_name,
      //   last_name,
      //   email,
      //   password,
      // });
      // setError('');
      // dispatch({ type: 'AUTH_SUCCESS', data: data });
    } catch (error) {
      // setLoading(false);
      // setSuccess('');
      // setError(error.response.data.message);
      // dispatch({ type: 'AUTH_FAIL' });
    }
  };
  return (
    <Container>
      <Grid container>
        <Paper sx={{ width: '100%', background: '#faf3f3' }}>
          <Typography variant="h4" component="h4" align="center">
            Signup
          </Typography>
          <form
            noValidate
            onSubmit={registerSubmit}
            align="center"
            sx={{ width: '100%' }}
          >
            <TextField
              id="first_name"
              label="First Name"
              name="first_name"
              variant="outlined"
              fullWidth
              required
              sx={{
                marginTop: {
                  xs: 1,
                  md: 2,
                  lg: 3,
                },
                width: '90%',
                alignSelf: 'center',

                marginBottom: 1,
              }}
              onChange={handleRegisterChange}
              error={firstNameError}
            />

            <TextField
              id="last_name"
              label="Last Name"
              name="last_name"
              variant="outlined"
              fullWidth
              required
              sx={{
                marginTop: {
                  xs: 1,
                  md: 2,
                  lg: 3,
                },
                width: '90%',
                alignSelf: 'center',

                marginBottom: 1,
              }}
              onChange={handleRegisterChange}
              error={lastNameError}
            />

            <TextField
              type="email"
              id="email"
              label="Email"
              name="email"
              variant="outlined"
              fullWidth
              required
              sx={{
                marginTop: {
                  xs: 1,
                  md: 2,
                  lg: 3,
                },
                width: '90%',
                alignSelf: 'center',

                marginBottom: 1,
              }}
              onChange={handleRegisterChange}
              error={emailError}
            />

            <FormControl sx={{ m: 1, width: '90%' }} variant="outlined">
              <InputLabel htmlFor="password">Password</InputLabel>
              <OutlinedInput
                id="password"
                name="password"
                type={showPassword ? 'text' : 'password'}
                endAdornment={
                  <InputAdornment position="end">
                    <IconButton
                      aria-label="toggle password visibility"
                      onClick={handleClickShowPassword}
                      onMouseDown={handleMouseDownPassword}
                      edge="end"
                    >
                      {showPassword ? <VisibilityOff /> : <Visibility />}
                    </IconButton>
                  </InputAdornment>
                }
                label="Password"
                onChange={handleRegisterChange}
                error={passwordError}
              />
            </FormControl>

            <FormControl sx={{ m: 1, width: '90%' }} variant="outlined">
              <InputLabel htmlFor="password">Password</InputLabel>
              <OutlinedInput
                id="confirmPassword"
                name="confirmPassword"
                type={showPassword ? 'text' : 'password'}
                endAdornment={
                  <InputAdornment position="end">
                    <IconButton
                      aria-label="toggle password visibility"
                      onClick={handleClickShowPassword}
                      onMouseDown={handleMouseDownPassword}
                      edge="end"
                    >
                      {showPassword ? <VisibilityOff /> : <Visibility />}
                    </IconButton>
                  </InputAdornment>
                }
                label="Password"
                onChange={handleRegisterChange}
                error={confirmPasswordError}
              />
            </FormControl>

            <Button
              variant="contained"
              color="sixth"
              type="submit"
              align="center"
              sx={{
                width: '80%',
                color: 'white',
                fontWeight: '700',
                marginBottom: 2,
              }}
            >
              Log In
            </Button>
          </form>
        </Paper>
      </Grid>
    </Container>
  );
};

export default RegisterForm;

登录页打开Modal的代码

const [open, setOpen] = React.useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(true);
...
...

<Modal
      open={open}
      onClose={handleClose}
      aria-labelledby="modal-modal-title"
      aria-describedby="modal-modal-description"
    >
      <Box sx={style}>
        <RegisterForm />
      </Box>
    </Modal>
解决方案

1. 阻止表单默认提交行为

表单默认提交会触发页面刷新,直接导致Modal关闭,必须在提交函数中阻止这一行为:
修改registerSubmit函数,接收event参数并调用event.preventDefault():

const registerSubmit = async (event) => {
  event.preventDefault(); // 阻止默认提交行为
  // 原有校验逻辑...
};

2. 完善校验逻辑,统一判断错误状态

添加变量统一判断是否存在错误,只有无错误时才执行后续提交逻辑:

const registerSubmit = async (event) => {
  event.preventDefault();
  
  setFirstNameError(false);
  setLastNameError(false);
  setEmailError(false);
  setPasswordError(false);
  setConfirmPasswordError(false);
  let hasError = false;

  dispatch({ type: 'AUTH_REQUEST' });
  try {
    if (!user.first_name.trim()) {
      setFirstNameError(true);
      hasError = true;
    }
    if (!user.last_name.trim()) {
      setLastNameError(true);
      hasError = true;
    }
    if (!user.email.trim()) {
      setEmailError(true);
      hasError = true;
    }
    if (!user.password.trim()) {
      setPasswordError(true);
      hasError = true;
    }
    if (user.password !== user.confirmPassword) {
      setConfirmPasswordError(true);
      toast.error('密码不匹配');
      hasError = true;
    }

    // 只有无错误时才发送请求
    if (!hasError) {
      const { data } = await axios.post('/api/users/register', {
        first_name,
        last_name,
        email,
        password,
      });
      setError('');
      dispatch({ type: 'AUTH_SUCCESS', data: data });
      // 注册成功后关闭Modal
      props.handleClose();
    }
  } catch (error) {
    setLoading(false);
    setSuccess('');
    const errorMsg = error.response?.data?.message || '注册失败';
    setError(errorMsg);
    toast.error(errorMsg);
    dispatch({ type: 'AUTH_FAIL' });
    // 错误时不关闭Modal
  }
};

3. 传递Modal状态控制函数给子组件

当前RegisterForm无法控制Modal的关闭状态,需要在登录页将handleClose传递给它:

// 登录页修改Modal内的RegisterForm
<RegisterForm handleClose={handleClose} />

然后在RegisterForm中接收这个props:

const RegisterForm = ({ handleClose }) => {
  // 组件内逻辑...
};

4. 修正登录页的handleClose函数

原代码中handleClose设置setOpen(true)是错误的,应该设置为false:

const handleClose = () => setOpen(false);

通过以上修改,当表单存在校验错误或后端返回错误时,Modal会保持打开状态,只有注册成功后才会关闭。

内容的提问来源于stack exchange,提问作者Stephen Scott Moore

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 18:37:02