如何在表单验证失败时保持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
相关产品推荐
相关产品推荐

