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

返回其他页面后,注册表单仍弹出错误Toast通知问题排查

问题描述

注册失败后切换至其他页面,再返回注册表单时,即便表单已清空,错误Toast仍会再次弹出。例如注册失败后跳转至登录页面,再回到注册表单,错误Toast会立即弹出。

附代码:

import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { signupForm, resetSignupState} from '../redux/actionSignup';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';

const Signup = () => {
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const { isSignedUp, errors: signupErrors} = useSelector((state) => state.signup);

  const [formData, setFormData] = useState({ //ok
    username: '',
    email: '',
    password: '',
    confirmpassword: '',
  });

  useEffect(() => {
    dispatch(resetSignupState());
  }, [dispatch]);

  const handleChange = (e) => { //ok
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value,
    });
  };

  const validateForm = () => { //ok
    const newErrors = {};
    if (!formData.username.trim()) {
      newErrors.username = 'Username is not valid';
    }
    if (!formData.email.includes('@gmail.com')) {
      newErrors.email = 'Invalid email!';
    }
    if (formData.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters!';
    }
    if (formData.password !== formData.confirmpassword) {
      newErrors.confirmpassword = 'Password does not match!';
    }
    return newErrors;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const newErrors = validateForm();
    
    dispatch(signupForm({ 
      ...formData, 
      errors: newErrors,
      isValid: Object.keys(newErrors).length === 0 
    }));
  };
  

  useEffect(() => {
    if (isSignedUp) {
      toast.success('Signed Up Successfully!', {
        className: 'custom-toast-success',
      });
      setFormData({
        username: '',
        email: '',
        password: '',
        confirmpassword: '',
      });
      dispatch(resetSignupState());
      navigate('/login');
    }
  }, [isSignedUp, dispatch, navigate]);

  useEffect(() => {
    if (signupErrors && Object.keys(signupErrors).length > 0) {
      toast.error('Signed Up Failed!', {
        className: 'custom-toast-error',
      });
    }
  }, [signupErrors]);

  useEffect(() => {
    dispatch(resetSignupState());
    setFormData({
      username: '',
      email: '',
      password: '',
      confirmpassword: '',
    });
  }, [dispatch]);

  return (
    <div className='signup-template d-flex justify-content-center align-items-center vh-100'>
      <div className='signupform-container p-5 rounded'>
        <form onSubmit={handleSubmit}>
          <h1 className='text-center signup-title'>Sign Up</h1>

          <div className='text-start mb-2'>
            <label htmlFor='username' className='signup-props'>Username</label>
            <input
              type='text'
              placeholder='Enter Username'
              className='form-control signup-list color-background'
              style={{ border: '1.4px solid black' }}
              id='username'
              name='username'
              value={formData.username}
              onChange={handleChange}
            />
            {signupErrors.username && <div className='error' style={{ color: 'crimson' }}>{signupErrors.username}</div>}
          </div>

          <div className='text-start mb-2'>
            <label htmlFor='email' className='signup-props'>Email</label>
            <input
              type='email'
              placeholder='Enter Email'
              className='form-control signup-list color-background'
              style={{ border: '1.4px solid black' }}
              id='email'
              name='email'
              value={formData.email}
              onChange={handleChange}
            />
            {signupErrors.email && <div className='error' style={{ color: 'crimson' }}>{signupErrors.email}</div>}
          </div>

          <div className='text-start mb-2'>
            <label htmlFor='password' className='signup-props'>Password</label>
            <input
              type='password'
              placeholder='Enter Password'
              className='form-control signup-list color-background'
              style={{ border: '1.4px solid black' }}
              id='password'
              name='password'
              value={formData.password}
              onChange={handleChange}
            />
            {signupErrors.password && <div className='error' style={{ color: 'crimson' }}>{signupErrors.password}</div>}
          </div>

          <div className='text-start mb-2'>
            <label htmlFor='confirmpassword' className='signup-props'>Confirm Password</label>
            <input
              type='password'
              placeholder='Confirm Password'
              className='form-control signup-list color-background'
              style={{ border: '1.4px solid black' }}
              id='confirmpassword'
              name='confirmpassword'
              value={formData.confirmpassword}
              onChange={handleChange}
            />
            {signupErrors.confirmpassword && <div className='error' style={{ color: 'crimson' }}>{signupErrors.confirmpassword}</div>}
          </div>

          <div className='text-center mt-3 signup-buttonsz'>
            <button type='submit' className='signup-button' name='submit'>
              Submit
            </button>
          </div>
        </form>
      </div>
    </div>
  );
};

export default Signup;
问题定位
  1. 重复的重置逻辑:代码里有两个完全相同的useEffect,都在调用resetSignupState()和清空表单,属于冗余代码,且可能导致状态重置时机混乱。
  2. Toast触发条件太宽泛:监听signupErrors的useEffect,只要signupErrors有内容就触发Toast。当从其他页面返回时,Redux中残留的旧错误状态会被读取,此时组件还没完成状态重置,Toast就先弹出来了。
  3. 状态重置与Toast触发的顺序问题:组件挂载时,重置状态的useEffect和触发Toast的useEffect执行顺序不确定,大概率是Toast先基于旧状态弹出,之后才执行状态重置。
修复方案

步骤1:移除重复的重置Effect

删掉其中一个完全重复的useEffect,只保留一个,避免冗余执行:

// 保留这个,删除另一个完全相同的useEffect
useEffect(() => {
  dispatch(resetSignupState());
  setFormData({
    username: '',
    email: '',
    password: '',
    confirmpassword: '',
  });
}, [dispatch]);

步骤2:优化Toast触发逻辑,区分首次挂载与提交后错误

通过组件内状态标记是否完成首次挂载,只有挂载完成后(即用户可能提交表单后)的错误才触发Toast:

const Signup = () => {
  // 新增状态:标记组件是否完成首次挂载
  const [isMounted, setIsMounted] = useState(false);
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const { isSignedUp, errors: signupErrors} = useSelector((state) => state.signup);

  // ...其他原有代码

  // 修改重置状态的Effect,挂载完成后标记状态
  useEffect(() => {
    dispatch(resetSignupState());
    setFormData({
      username: '',
      email: '',
      password: '',
      confirmpassword: '',
    });
    setIsMounted(true);
  }, [dispatch]);

  // 修改Toast触发逻辑:仅在挂载完成且有错误时弹出
  useEffect(() => {
    if (isMounted && signupErrors && Object.keys(signupErrors).length > 0) {
      toast.error('Signed Up Failed!', {
        className: 'custom-toast-error',
      });
    }
  }, [signupErrors, isMounted]);

  // ...其他原有代码
};

步骤3:确保Redux状态重置的完整性

检查resetSignupState对应的reducer,确保所有相关状态都被重置,比如把errors清空:

// 示例reducer代码,根据你的实际reducer调整
const initialState = {
  isSignedUp: false,
  errors: {},
  // 其他状态字段...
};

const signupReducer = (state = initialState, action) => {
  switch(action.type) {
    case 'RESET_SIGNUP_STATE':
      return {
        ...initialState, // 或者明确重置每个字段
        isSignedUp: false,
        errors: {}
      };
    // ...其他case逻辑
  }
};

可选优化:通过Redux状态标记提交状态(更严谨)

如果想彻底区分“用户提交产生的错误”和“旧状态残留错误”,可以在Redux的signup状态中新增hasSubmitted字段:

  1. 在signupForm action中,提交表单时将hasSubmitted设为true
  2. 在resetSignupState action中,将hasSubmitted设为false,同时清空errors
  3. 组件中修改Toast触发逻辑:
// 从Redux中取出hasSubmitted
const { isSignedUp, errors: signupErrors, hasSubmitted} = useSelector((state) => state.signup);

// ...其他代码

useEffect(() => {
  // 仅当用户提交过且存在错误时弹出Toast
  if (hasSubmitted && signupErrors && Object.keys(signupErrors).length > 0) {
    toast.error('Signed Up Failed!', {
      className: 'custom-toast-error',
    });
    // 弹出后重置状态,避免再次返回时触发
    dispatch(resetSignupState());
  }
}, [signupErrors, hasSubmitted, dispatch]);

内容的提问来源于stack exchange,提问作者A Boy Learns to Code

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 12:35:57