DEV Community

Peter Jacxsens
Peter Jacxsens

Posted on • Updated on

12/ Error handling in our form component for the NextAuth CredentialsProvider

All the code for this chapter is available on github: branch credentialssignin.

We have some things left to handle:

  1. Form input validation
  2. Handle errors returned by signIn
  3. Handle a successful sign in
  4. Handle loading states

Form validation

We will validate our input using client-side zod. Zod handles TypeScript-first schema validation with static type inference. This means that it will not only validate your fields, it will also set types on validated fields.

I'm not very familiar with Zod myself but it seems to work great with server actions and the useFormState hook. We will be using it in this context later on but for now we will use it client side. Install Zod:

npm install zod
Enter fullscreen mode Exit fullscreen mode

We need to validate the username/email and password input fields. These are both inside our data useState hook that we set up earlier. We start by creating a formSchema:

const formSchema = z.object({
  identifier: z.string().min(2).max(30),
  password: z
    .string()
    .min(6, { message: 'Password must be at least 6 characters long.' })
    .max(30),
});
Enter fullscreen mode Exit fullscreen mode

This is pretty self explanatory. We want both fields to be strings and have a minimum and maximum length. We provide a custom error message when the password is too short. Note, there is plenty of password validation articles and opinions out there. Handle this yourself.

In our handleSubmit, before we call signIn, we call this formSchema. It will check if our input field values matches the conditions we set in our formSchema. By using the validatedFields.success property, we can handle what needs to happen next.

On error, Zod will generate error messages for us that we save in state. On success,we can simply call the signIn function as our inputs are valid.

type FormErrorsT = {
  identifier?: undefined | string[],
  password?: undefined | string[],
};

const [errors, setErrors] = useState < FormErrorsT > {};

const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();

  const validatedFields = formSchema.safeParse(data);
  if (!validatedFields.success) {
    setErrors(validatedFields.error.formErrors.fieldErrors);
  } else {
    // no zod errors
    // call signIn
  }
};
Enter fullscreen mode Exit fullscreen mode

Finally, update our form jsx to display the error messages. for each input field we do something like this:

{
  errors?.identifier ? (
    <div className='text-red-700' aria-live='polite'>
      {errors.identifier[0]}
    </div>
  ) : null;
}
Enter fullscreen mode Exit fullscreen mode

and a general error below the form:

{
  errors.password || errors.identifier ? (
    <div className='text-red-700' aria-live='polite'>
      Something went wrong. Please check your data.
    </div>
  ) : null;
}
Enter fullscreen mode Exit fullscreen mode

NextAuth CredentialsProvider form validation

To summarize: on submit we use Zod to validate our fields. If there is no error, we call signIn. If there is an error, Zod gives us an error object, mirroring our form state. We save this error inside an error state and use it to display user feedback.

Handle errors returned by NextAuth signIn function

In the previous chapter we handled Strapi errors by throwing errors in our authorize function. In our frontend, we added a property redirect: false to the options object of our signIn function. Doing this makes signIn return an object with an error property:

{
  error?: string;
  status: number;
  ok: boolean;
  url?: string;
}
Enter fullscreen mode Exit fullscreen mode

Now we will use this object to provider feedback to the user. We check if there is an error and then put this error in our error state that we already used for Zod errors. We extend our error Type with a strapiError property:

type FormErrorsT = {
  identifier?: undefined | string[];
  password?: undefined | string[];
  strapiError?: string;
};
Enter fullscreen mode Exit fullscreen mode

Listen for an error on the signInResponse and put it in the error state:

if (signInResponse && !signInResponse?.ok) {
  setErrors({
    strapiError: signInResponse.error
      ? signInResponse.error
      : 'Something went wrong.',
  });
} else {
  // handle success
}
Enter fullscreen mode Exit fullscreen mode

Below our form we display the strapiError:

{
  errors.strapiError ? (
    <div className='text-red-700' aria-live='polite'>
      Something went wrong: {errors.strapiError}
    </div>
  ) : null;
}
Enter fullscreen mode Exit fullscreen mode

We are now displaying custom errors we threw inside authorize in our form component!

Strapi error in form

Handle a successful sign in

We haven't actually handled a successful sign in process with credentials. We know we have successfully signed in when we have no errors. But what to do next? Previously, we set up our GoogleProvider to redirect to the previous page on successful sign in.

This isn't a solution I would adopt in production. For example, if the previous page was the sign up page then it would be bad UX to send them back to register page after signing in. But, I will leave this to you and stick with simply redirecting.

As before, we simply get our url from the callbackUrl searchParam:

const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl') || '/';
const router = useRouter();
Enter fullscreen mode Exit fullscreen mode

And then we just push our new route:

router.push(callbackUrl);
Enter fullscreen mode Exit fullscreen mode

Note here: the signIn function options object has a callbackUrl property. You can use this if you like to redirect to a fixed page.

Let's test what we just wrote. Run our app and try logging in with (correct) credentials. Everything works, we are redirected but:

update getServerSession

Client versus server session

Well, something went wrong! The obvious problem here is that our <LoggedInClient /> component (uses useSession hook) says we're logged in but our server component <LoggedInServer /> (uses getServerSession) says we're not. Our <NavbarUser /> component (uses getServerSession) is also failing. It should show the username followed by the <SignOutButton /> yet it shows no username and the sign in link.

So, there seems to be a problem with getServerSession. It doesn't seem to update. And yes, we are signed in, so useSession is correct here. Turning to the NextAuth docs gave no answers. There is no NextAuth way to manually trigger a getServerSession refresh.

I spend quite some time trying to figure this out. The breaking change we made was adding the redirect: false option. It's been a while since I mentioned this but when we temporarily remove this redirect option and try logging in we notice something. There is a full page refresh! That means that Next and NextAuth get fresh, updated data.

With the redirect: false option back, when successfully logging in with credentials we do a router.push. This does not trigger a page refresh, as we would expect from a single page app. But this also means that our server components like <LoggedInServer /> and <NavbarUser /> aren't refreshed and display a stale state.

That's what is causing our problem. How do we solve this? I just looked around, googled and read a lot until somebody mentioned router.refresh. From the Next docs:

router.refresh(): Refresh the current route. Making a new request to the server, re-fetching data requests, and re-rendering Server Components. The client will merge the updated React Server Component payload without losing unaffected client-side React (e.g. useState) or browser state (e.g. scroll position).

And this solves the problem. We add the line and test again:

// handle success
router.push(callbackUrl);
router.refresh();
Enter fullscreen mode Exit fullscreen mode

Server and client signed in

Couple of remarks here:

  • This is a solid solution. NextAuth maybe dropped the ball here but using router.refresh solves this perfectly. This is not a hack and may be the only way to solve this.
  • When actually doing this, there is a slight delay in the server component. The client component will update first, followed by the server component. Just noting.
  • router.refresh may cause a screen flicker. Don't worry, this is a dev mode thing. It disappears when you run the app in production mode (next build + next start). (This took me some time to figure out :/)

Setting loading states

Let's account for furious button smashing on a slower connection by disabling our button while we're doing our sign in thing.

Create a state for loading and initiate it to false. When we submit we set loading to true. On Zod error or strapi error we set it to false. Update our button with the disabled attribute and some disabled styles:

<button
  type='submit'
  className='bg-blue-400 px-4 py-2 rounded-md disabled:bg-sky-200 disabled:text-gray-400 disabled:cursor-wait'
  disabled={loading}
  aria-disabled={loading}
>
  sign in
</button>
Enter fullscreen mode Exit fullscreen mode

Done! Take a look at the finished <SignIn /> component on github.

Conclusion

We just set up a credentials sign in flow and it took some work (3 chapters)! Here is a run-down of what we did:

  • Adding the CredentialsProvider to the NextAuth providers.
  • Setting up a form with controlled inputs.
  • Calling signIn client side with the credentials.
  • Explaining and writing out the authorize function/callback.
  • Calling the Strapi sign in endpoint.
  • Returning a response from authorize.
  • Updating the jwt callback.
  • Fixing our callback Types.
  • Exploring NextAuth default error handling for the authorize function.
  • Handling Strapi errors in authorize.
  • Updating signIn with redirect: false.
  • Adding form validation with Zod.
  • Handling the return value from signIn.
  • Redirecting the user on a successful sign in.
  • Fixing getServerSession not reloading.
  • Adding loading states.

Auth flows are difficult. You should by now start getting a real good feel for NextAuth and how you can use it in your own projects. Hopefully I was able to clearly explain everything and help you save some time. In the next chapter, we will be implementing a sign up flow for the CredentialsProvider.


If you want to support my writing, you can donate with paypal.

Top comments (0)