Search code examples
reactjstypescriptreduxredux-toolkit

Why dispatch works with delay? React Redux


I have that simple code:

const Login = (): JSX.Element => {
  const {errors}= useAppSelector(state => state.errorsReducer)
  const dispatch = useAppDispatch();

  const navigate = useNavigate();
  
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const onSubmit = (e: FormEvent) => {
    e.preventDefault();
  };

  const login = () => {
    dispatch(errorsSlice.actions.clearErrors());
    console.log(errors);
    const userData = {
      email: email,
      password: password
    }
    
    if(!userData.email || !userData.password){
      dispatch(errorsSlice.actions.addError('Email or password is undefined'));
    }

    if(errors.length === 0){
      axios
        .post('http://localhost:7000/auth/login', userData)
        .then(response =>{
          const decodedToken: IUser = jwt_decode(response.data.token);
          localStorage.setItem('userData', JSON.stringify(decodedToken));
          localStorage.setItem('token', JSON.stringify(response.data.token));
          navigate('/')
        })
        .catch(e => {
          const errorMessage = e?.response?.data?.message
          dispatch(errorsSlice.actions.addError(errorMessage));
        });
    }
    console.log(errors);
  }

    return (
      <main className="form-signin m-auto"  style={{height: 600}}>
        <div className='container h-100 d-flex align-items-center justify-content-center'>
          <form onSubmit={(e) => onSubmit(e)} style={{width: 400}}>
            <h1 className="h3 mb-3 fw-normal">Auth</h1>

            <div className="form-floating">
              <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="form-control" id="floatingInput" placeholder="[email protected]"/>
              <label htmlFor="floatingInput">Email</label>
            </div>
            <div className="mt-2 form-floating">
              <input type="password" className="form-control" value={password} onChange={(e) => setPassword(e.target.value)} id="floatingPassword" placeholder="Password"/>
              <label htmlFor="floatingPassword">Password</label>
            </div>

            <div className="checkbox mt-2 mb-3">
              <label>
                Or <Link to='/registration'>click that</Link> to registration. 
              </label>
            </div>
            <button className="w-100 btn btn-lg btn-primary" onClick={() => login()} type="submit">Login</button>
          </form>
        </div>

        <>
          {errors.map((error) => {
              return <MyToast
                text = {error}
                color = {'bg-danger'}
                show = {true}
                key = {error}
              />
            })}
          </>
        
    </main>
    )
}

On click Login button call function login() which in case no errors send request on server. I testing requests with errors. That works on first time, dispatch set error to store and all works good, but on second click should work dispatch(errorsSlice.actions.clearErrors()), but his work with delay and when my function go to expression if(errors.length === 0), then expression return false, because errors.length = 1, although dispatch(errorsSlice.actions.clearErrors()) should have worked. What me need do, to that code works true?


Solution

  • The selected errors state value is closed over in login callback scope, it will never be a different value in the callback. You can't dispatch actions to update the state and expect the closed over values to change.

    I suspect you are really just wanting to validate the inputs and dispatch the addError action if there are issues, otherwise continue with the login flow. I suggest a small refactor to compute and check local errors to avoid the issue of the stale closure over the selected errors state.

    Example:

    const Login = (): JSX.Element => {
      const { errors } = useAppSelector(state => state.errorsReducer)
      const dispatch = useAppDispatch();
    
      const navigate = useNavigate();
      
      const [email, setEmail] = useState('')
      const [password, setPassword] = useState('')
    
      const onSubmit = async (e: FormEvent) => {
        e.preventDefault();
      
        dispatch(errorsSlice.actions.clearErrors());
        
        // check for field data issue, early return
        if (!email || !password){
          dispatch(errorsSlice.actions.addError('Email or password is undefined'));
          return;
        }
    
        // no field data issues, try authenticating
        try {
          const userData = { email, password };
          const response = await axios.post(
            "http://localhost:7000/auth/login",
            userData
          );
    
          const decodedToken: IUser = jwt_decode(response.data.token);
          localStorage.setItem('userData', JSON.stringify(decodedToken));
          localStorage.setItem('token', JSON.stringify(response.data.token));
          navigate('/');
        } catch(e) {
          const errorMessage = e?.response?.data?.message
          dispatch(errorsSlice.actions.addError(errorMessage));
        };
      }
    
      return (
        <main className="form-signin m-auto"  style={{ height: 600 }}>
          <div className="container h-100 d-flex align-items-center justify-content-center">
            <form onSubmit={onSubmit} style={{width: 400}}>
              <h1 className="h3 mb-3 fw-normal">Auth</h1>
    
              <div className="form-floating">
                <input
                  type="email"
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  className="form-control"
                  id="floatingInput"
                  placeholder="[email protected]"
                />
                <label htmlFor="floatingInput">Email</label>
              </div>
              <div className="mt-2 form-floating">
                <input
                  type="password"
                  className="form-control"
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  id="floatingPassword"
                  placeholder="Password"
                />
                <label htmlFor="floatingPassword">Password</label>
              </div>
    
              <div className="checkbox mt-2 mb-3">
                <label>
                  Or <Link to='/registration'>click that</Link> to registration. 
                </label>
              </div>
              <button className="w-100 btn btn-lg btn-primary" type="submit">
                Login
              </button>
            </form>
          </div>
          <>
            {errors.map((error) => (
              <MyToast
                key={error}
                color="bg-danger"
                show
                text={error}
              />
            ))}
          </>
        </main>
      )
    }