12/ Error handling in our form component for the NextAuth CredentialsProvider
Tags: nextauth, nextjs, credentialsprovider
Published: 2024-04-01 16:29:37

All the code for this chapter is available on [github: branch credentialssignin](https://github.com/peterlidee/NNAS/tree/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](https://zod.dev/). 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
```

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:

```jsx
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),
});
```

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.

```jsx
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
  }
};
```

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

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

and a general error below the form:

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

![NextAuth CredentialsProvider form validation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vrar32s2mj0bst2y8d1i.png)

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:

```ts
{
  error?: string;
  status: number;
  ok: boolean;
  url?: string;
}
```

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:

```ts
type FormErrorsT = {
  identifier?: undefined | string[];
  password?: undefined | string[];
  strapiError?: string;
};
```

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

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

Below our form we display the strapiError:

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

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

![Strapi error in form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w43qglr7979ead4i4oy3.png)

## 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:

```jsx
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl') || '/';
const router = useRouter();
```

And then we just push our new route:

```jsx
router.push(callbackUrl);
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/46hvlef0rm4whdsn2gsk.png)

## 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](https://nextjs.org/docs/app/api-reference/functions/use-router#userouter):

> 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:

```jsx
// handle success
router.push(callbackUrl);
router.refresh();
```

![Server and client signed in](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3s1ggbx5ery6sqgr0hds.png)

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:

```jsx
<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>
```

Done! Take a look at the finished `<SignIn />` component on [github](https://github.com/peterlidee/NNAS/blob/credentialssignin/frontend/src/components/auth/signIn/SignInForm.tsx).

## 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](https://www.paypal.com/donate/?hosted_button_id=4D78YQU4V5NEJ).