skip to Main Content

In my client I have:

const [error, loginUser, pending] = useActionState(login, null);

loginUser gets called from a login form:

<form action={loginUser}>....</form>

error keeps track of the error returned and if not-null then it shows a toast notification:

 useEffect(() => {
   // this will only fire if the error changes ;(
   error && toast.error(error);
}, [error]);

This works the first time a user encounters an error (like the password is too short, but it could be anything etc). But, if the user tries again and encounters the same error then no new toast is shown…but it should even though it is the same error message. Although this is my login page I need the same functionality for my registration page.

I’ve tried the following:

let [error, register, pending] = useActionState(registerUser, null);
useEffect(() => {
   // this will only fire if the error changes ;(
   error && toast.error(error);
   error = null; // reset the error so it triggers useEffect() again..still doesn't work ;(
}, [error]);

But, sadly, this still doesn’t work. Any suggestions?

2

Answers


  1. You can use the setup:

    $color{green}{test}$

    'use client';
    
    import React from 'react';
    
    import {
      LoginFormSchemaType,
      loginFormSchema,
    } from '@/schemas/auth-schemas';
    import { IoThumbsUpOutline } from 'react-icons/io5';
    import { cn } from '@/util/cn';
    
    import ShadowWrapper from '@/components/ui/ShadowWrapper';
    import Button from '@/components/ui/Button';
    import Text from '@/components/ui/Text';
    import CircleIllustration from '@/components/ui/CircleIllustraion';
    import Fade from '@/components/ui/Fade';
    
    import { Controller, SubmitHandler, useForm } from 'react-hook-form';
    import { zodResolver } from '@hookform/resolvers/zod';
    import { useAction } from 'next-safe-action/hooks';
    import { loginSafeAction } from '@/server/auth-actions';
    import { useAppDispatch } from '@/store';
    import { setTwoFaUserData } from '@/store/authSlice';
    import ServerError from '@/components/ui/ServerError';
    import Link from 'next/link';
    import { LINKS } from '@/config';
    import { useRouter } from 'next/navigation';
    import { getRandomId } from '@/util';
    import Input from '@/components/ui/Input';
    
    export default function LoginForm() {
      const dispatch = useAppDispatch();
      const router = useRouter();
      const {
        handleSubmit,
        control,
        formState: { errors, isValid },
        reset,
      } = useForm<LoginFormSchemaType>({
        resolver: zodResolver(loginFormSchema),
        mode: 'onSubmit',
      });
      const {
        execute: login,
        status: loginStatus,
        result,
        reset: resetLoginAction,
      } = useAction(loginSafeAction, {
        onSuccess: data => {
          if ('success' in data) {
            reset({ email: '', otp: '', password: '' });
            router.push(LINKS.PROFILE);
          }
        },
      });
      const onSubmit: SubmitHandler<
        LoginFormSchemaType
      > = async formValues => {
        login(formValues);
        resetLoginAction();
      };
      return (
        <div className='flex bg-white lg:px-0 sm:px-6 px-4 items-center w-full flex-1'>
          <Fade
            towards='X'
            x={-5}
            duration={0.35}
            initialOpacity={0}
            finalOpacity={1}
            className='max-w-xl relative bg-white z-10 w-full rounded-lg   mx-auto'
          >
            <ShadowWrapper className='sm:p-8 p-4'>
              <ServerError
                key={getRandomId()}
                onHide={resetLoginAction}
                error={result.data?.error}
              />
              <Text
                as='h1'
                color='gray'
                noMargin
                className='flex items-center gap-2'
              >
                <IoThumbsUpOutline />
                <Text
                  as='span'
                  noMargin
                  color='gray'
                  className='font-[500]'
                  size='lg'
                >
                  Welcome back
                </Text>
              </Text>
              <Text
                className='font-normal'
                as='h2'
                color='gray700'
                noMargin
                size='md'
              >
                Your satisfaction is our priority, and we&lsquo;re
                committed to making your experience as smooth as possible
              </Text>
              <form onSubmit={handleSubmit(onSubmit)} className='pt-6'>
                <Controller
                  name='email'
                  control={control}
                  render={({ field }) => (
                    <Input
                      errors={errors}
                      placeholder='[email protected]'
                      field={field}
                      type='text'
                      label='Email'
                      fieldName='email'
                    />
                  )}
                />
                <Controller
                  control={control}
                  name='password'
                  render={({ field }) => (
                    <Input
                      errors={errors}
                      placeholder='*******'
                      field={field}
                      type='password'
                      label='Password'
                      fieldName='password'
                    />
                  )}
                />
    
                <Button
                  type='submit'
                  isLoading={loginStatus === 'executing'}
                  size='xl'
                  color={isValid ? 'yellow' : 'borderedBlack'}
                  className={cn('rounded-full  w-full mt-8')}
                >
                  <Text
                    color='gray'
                    className='text-lg block font-semibold '
                  >
                    Login
                  </Text>
                </Button>
    
                <Text
                  noMargin
                  as='span'
                  className='flex justify-end'
                  color='error'
                >
                  <Link
                    className='pt-1 text-end'
                    href={LINKS.FORGOT_PASSWORD}
                  >
                    Forget password
                  </Link>
                </Text>
              </form>
            </ShadowWrapper>
          </Fade>
         
        </div>
      );
    }
    

    ServerErrror Component code:

    'use client';
    import { cn } from '@/util/cn';
    import { motion, AnimatePresence } from 'framer-motion';
    import React, { ReactNode, useEffect, useState } from 'react';
    import { MdOutlineErrorOutline } from 'react-icons/md';
    import { IoMdClose } from 'react-icons/io';
    import Show from '@/components/ui/Show';
    export default function ServerError({
      error,
      className,
      hideAfterSecond = 5,
      onHide,
    }: {
      error?: ReactNode;
      className?: string;
      hideAfterSecond?: number;
      onHide: () => void;
    }) {
      const [hide, setHide] = useState(false);
      const [remainingSecond, setRemainingSecond] =
        useState(hideAfterSecond);
      useEffect(() => {
        if (!error) return;
        if (hide || remainingSecond <= 0) return setHide(true);
        const timer = setTimeout(() => {
          setRemainingSecond(preState => preState - 1);
        }, 1000);
        return () => {
          clearTimeout(timer);
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [remainingSecond, hide, setHide]);
    
      useEffect(() => {
        if (!error) return;
        if (hide) onHide();
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [hide, error]);
      if (!error) return;
      const errorMessage =
        typeof error === 'string' && error.length <= 1
          ? 'Unexpected error from server. Please try again later.'
          : error;
    
      return (
        <AnimatePresence>
          <Show when={!hide}>
            <motion.div
              key='error'
              className={cn(
                'server-error py-4 px-5 text-lg rounded-xl duration-300 relative my-2 text-black bg-error',
                className
              )}
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
            >
              <p className='flex items-center text-charcoal gap-3'>
                <span>
                  <MdOutlineErrorOutline size={26} />
                </span>
                <span>{errorMessage}</span>
              </p>
              <IoMdClose
                onClick={() => setHide(true)}
                size={18}
                className='absolute right-0 top-0 mt-3 mr-4 cursor-pointer'
              />
            </motion.div>
          </Show>
        </AnimatePresence>
      );
    }
    
    

    Server Action:

    export const loginSafeAction = action(loginFormSchema, async body => {
      try {
        const res = await fetch(
          `${process.env.BACKEND_BASE}/users/login`,
          {
            method: REQUEST_METHODS.POST,
            headers: {
              ...getNodeRequiredRequestHeaders(),
              ...getJsonTypeHeader(),
            },
            body: JSON.stringify({
              email: body.email,
              password: body.password,
              token: body.otp,
            }),
          }
        );
        // NOTE check if 2fa is enabled
        if (res.status === 302) {
          throw new Error(LINKS.LOGIN_2FA);
        }
        // NOTE check if api call fails
        if (!res.ok) {
          const errorResponse = await getApiError(res);
          throw new Error(errorResponse.error);
        }
        const { data, jwt } = await res.json();
        // NOTE check if user is verified
        setSecureJwt(jwt);
    
        if (!data.active) {
          try {
            await fetch(
              `${process.env.BACKEND_BASE}/users/resend-signup-otp`,
              {
                method: REQUEST_METHODS.GET,
                headers: {
                  ...getNodeRequiredRequestHeaders(),
                  ...getAuthorizationHeader(),
                },
              }
            );
          } catch (reqeustSingUpOtpError: any) {
            //We don't need to keep track of errors in the case of resendOtp we just need to redirect him to the verification screen
          } finally {
            throw new Error(LINKS.SIGNUP_OTP);
          }
        }
        return { success: data as string };
      } catch (err: any) {
        if (err.message === LINKS.LOGIN_2FA) {
          redirect(LINKS.LOGIN_2FA);
        } else if (err.message === LINKS.SIGNUP_OTP) {
          redirect(LINKS.SIGNUP_OTP);
        } else {
          return { error: err.message };
        }
      }
    });
    

    util:

    export const action = createSafeActionClient();
    import { createSafeActionClient } from 'next-safe-action';
    
    

    Note:

    I am using the next safe action version 6.1.0 https://v6.next-safe-action.dev/ you can use its latest version if you want https://next-safe-action.dev/
    i have a little bit complex setup for the login because in my case i need to check for 2fa and user account activation cases too. But you can customize it to fit your needs

    Login or Signup to reply.
  2. Your error is a string, which is primitive. And of course the useEffect will not be triggered if it’s the same error.

    Your login needs to return an object, and its structure should be

    {
       id: Math.random(),
       message: 'some error message string'
    }
    
     useEffect(() => {
       error && toast.error(error.message);
    }, [error]);
    
    

    It should work. Try it.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search