14/ Signing up with NextAuth CredentialsProvider using server actions and useFormState
Tags: nextauth, nextjs, serveractions, useformstate
Published: 2024-04-01 16:32:53

This code for this example is available on [github: branch credentialssignup](https://github.com/peterlidee/NNAS/tree/credentialssignup).

We have our form, what's next? What are all the steps we still have to do?

- Call `Strapi` sign up endpoint.
- Handle error and success.
- Validate the fields.
- Display errors to the user.
- Send confirmation email.
- Set loading state.

In our sign in form that we made in a [previous chapter](https://dev.to/peterlidee/signing-up-with-the-nextauth-credentialsprovider-25m5) we set our entire data, error and loading state up ourselves. We had to because the `NextAuth signIn` function is a client-side only function.

However, we don't need `NextAuth` now. The only thing we need to do is call a `Strapi` endpoint and we can do that server side. How? Either setup a custom api endpoint in `Next` (a route handler) or even easier we're going to use a server action. What goes well with server actions? The `useFormState` and `useFormStatus` hooks to handle our data, error and loading states.

So, we will create a server action signUpAction to:

1. Validate the input fields using `Zod` (server side)
2. Call the `Strapi` sign up endpoint and handle error or success.

If everything goes as planned without errors, signUpAction will redirect the user to another page with a feedback message: 'we sent you an email...'. When there is an error (either `Zod` or `Strapi`), our server action send back a state with an error property to `useFormState`.

## signUpAction

Let's write this out. Since we use `useFormState`, our server action will receive 2 parameters:

1. prevState: current state of `useFormState`.
2. formData: interface that lets you read out and manipulate all the fields of your form.

We start by validating our fields using `Zod`:

```jsx
// frontend/src/components/auth/signup/signUpAction.ts

'use server';

import { z } from 'zod';

const formSchema = z.object({
  username: z.string().min(2).max(30).trim(),
  email: z.string().email('Enter a valid email.').trim(),
  password: z.string().min(6).max(30).trim(),
});

export default async function signUpAction(
  prevState: SignUpFormStateT,
  formData: FormData
) {
  const validatedFields = formSchema.safeParse({
    username: formData.get('username'),
    email: formData.get('email'),
    password: formData.get('password'),
  });

  if (!validatedFields.success) {
    return {
      error: true,
      fieldErrors: validatedFields.error.flatten().fieldErrors,
      message: 'Please verify your data.',
    };
  }

  const { username, email, password } = validatedFields.data;
  // do more stuff
}
```

The `Zod` validation should be clear. We created a schema, check if our input fields match the criteria. If they don't we return an object with an error property to `useFormState`. 2 Notes:

1. This will error because there is no SignUpFormStateT type yet, we will handle this later.
2. If you want more advanced password requirements, add them.

Next, we call `Strapi` with the validated fields. We wrap the entire call inside a try catch block.

```jsx
// do more stuff
try {
  const strapiResponse = await fetch(
    process.env.STRAPI_BACKEND_URL + '/api/auth/local/register',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ username, email, password }),
      cache: 'no-cache',
    }
  );

  // handle strapi error
  if (!strapiResponse.ok) {
    const response = {
      error: true,
      message: '',
    };
    // check if response in json-able
    const contentType = strapiResponse.headers.get('content-type');
    if (contentType === 'application/json; charset=utf-8') {
      const data = await strapiResponse.json();
      response.message = data.error.message;
    } else {
      response.message = strapiResponse.statusText;
    }
    return response;
  }
} catch (error) {
  // network error or something
  return {
    error: true,
    message: 'message' in error ? error.message : error.statusText,
  };
}
```

The fetch we covered earlier. We then check strapiResponse for an error. Note, not all `Strapi` error will be json, that if why we make the check `if (contentType === 'application/json; charset=utf-8')`. If there is an error, we return an object with some properties to `useFormState`. Same goes for the catch.

There is something missing here: success handling. On success, we will redirect the user to a message page using the `next/navigation` redirect function. But you [shouldn't call redirect inside a try catch block](https://nextjs.org/docs/app/building-your-application/routing/redirecting#redirect-function). So, we let ourself fall through the try catch block and only after it, we add the redirect:

```jsx
// page yet to be made
redirect('/confirmation/message');
```

I hope this makes sense. When the strapiResponse was successful, we want to redirect the user to another page. We don't need data from this response. When there is no error, javascript will just move to the next line in our server action and that is the redirect.

And we are done, except for the SignUpFormStateT type that we will cover in a bit.

## useFormState

We go back to our form now that we have our server action. We need to setup our `useFormState` hook. `useFormState` is a hook that couples server actions to state. Using `useFormState` means that you don't have to set state manually.

We initiate `useFormState` with a server action (signUpAction) and an initial state. It returns this state and a formAction. Basically a wrapper around the server action that we passed. Anything that we return from our server action will become our new state. When calling our server action, `useFormState` also passes this state and formData to our server action so we have access to this. We've seen this above in signUpAction: the prevState and formData parameters.

Let's add it to our form:

```jsx
const initialState = {
  error: false,
};
const [state, formAction] = useFormState(signUpAction, initialState);
// ...
<form className='my-8' action={formAction}></form>;
```

## How to set Types to useFormState and server actions

Setting Types on useFormState can be a bit tricky because you need to account to every possible return value of the server action as well as the initialState. It's something you have to think out and put effort into.

In case of our `signUpAction`: we know it only returns when there is an error: `Zod` error, `Strapi` error or a catch error. In case of success, it won't return but redirect. What did we return in these cases?

```jsx
{
  error: true,
  message: 'some error message'
}
```

Only the `Zod` error has an extra property, the `Zod` error object:

```jsx
export type InputErrorsT = {
  username?: string[],
  email?: string[],
  password?: string[],
};
```

Since `signUpAction` does not return on success, it's return Type looks like this:

```
{
  error: true,
  message: string,
  inputErrors?: InputErrorsT
}
```

With inputErrors (the `Zod` errors) being optional. Our initialState only has one property: `error: false`. We could then update our type by making message optional.

TypeScript will correctly infer this in the `useFormState` hook, without explicitly setting a Type:

![useFormState inferred type](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yhtn3ta18hucolxt98qt.png)

But, we do need to set an explicit style in our `signUpAction` action because that is how TypeScript works:

```jsx
export default async function signUpAction(
  prevState: SignUpFormStateT,
  formData: FormData
) {
  //...
}
```

Since we explicitly have to set it in the server action, we might as well set it explicitly in our `useFormState` hook as well. This is how we do that:

```jsx

export type InputErrorsT = {
  username?: string[];
  email?: string[];
  password?: string[];
};

export type RegisterFormStateT = {
  error: boolean;
  InputErrors?: InputErrorsT;
  message?: string;
};

// ...

const [state, formAction] = useFormState<SignUpFormStateT, FormData>(
  signUpAction,
  initialState
);
```

This setup works, it's correct and there are no TS errors. But we can do better. We are going to update our Type to a discriminated union. This makes sense: it will be either an error or the initialState. This is our update:

```jsx
type SignUpFormInitialStateT = {
  error: false; // not boolean
};
type SignUpFormErrorStateT = {
  error: true; // not boolean
  message: string; // not optional
  inputErrors?: InputErrorsT;
};
// discriminated union
export type SignUpFormStateT = SignUpFormInitialStateT | SignUpFormErrorStateT;
// explicitly set type here
const initialState: SignUpFormInitialStateT = {
  error: false,
};

// ...
const [state, formAction] = useFormState<SignUpFormStateT, FormData>(
  signUpAction,
  initialState
);
```

I hope this makes sense. This is a simple case and it's already getting complex. This is something that requires some effort in other words.

## Using state

We haven't actually used the state to display feedback to the user. Let's add that now. For each input we check for a corresponding `Zod` error and display it.

```jsx
<div className='mb-3'>
  <label htmlFor='username' className='block mb-1'>
    Username *
  </label>
  <input
    type='text'
    id='username'
    name='username'
    required
    className='border border-gray-300 w-full rounded-sm px-2 py-1'
  />
  {state.error && state?.inputErrors?.username ? (
    <div className='text-red-700' aria-live='polite'>
      {state.inputErrors.username[0]}
    </div>
  ) : null}
</div>

// do the other inputs too
```

Finally, on the bottom of the form we display the error.message:

```jsx
{
  state.error && state.message ? (
    <div className='text-red-700' aria-live='polite'>
      {state.message}
    </div>
  ) : null;
}
```

## Handling loading states with useFormStatus

This leaves us with one last task: setting loading state. For this we will of course use the [`useFormStatus` hook](https://react.dev/reference/react-dom/hooks/useFormStatus):

> useFormStatus is a Hook that gives you status information of the last form submission.

In `signIn`, we manually set loading states. Since we use `useFormState` in this form, we can also use `useFormStatus` to handle the loading state for us. These is one catch, we have to call `useFormStatus` in a separate component from where we call `useFormState`. Create a new component:

```jsx
// frontend/src/components/auth/PendingSubmitButton.tsx

import { useFormStatus } from 'react-dom';

export default function PendingSubmitButton() {
  const { pending } = useFormStatus();
  return (
    <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={pending}
      aria-disabled={pending}
    >
      send
    </button>
  );
}
```

And we insert it into our form component. And that's it. Sign up is complete. We do some quick tests:

- Entering inputs that are too short lead to `Zod` errors.

![Zod error on sign up](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a7o9899wcsxh9ao1svvx.png)

- Entering a username or email that is already in the DB leads to a `Strapi` error.

![sign up Strapi error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/frchs5hb5avi16sfadds.png)

- The submit button has a (very fast) loading state when submitting.
- On success we are redirected to a page that we haven't build yet.

Everything works!

## Conclusion

We just build our sign up page. We started with an issue with the _sign in with Google_ button. Because the error handling on this button only works on the _sign in_ page, we had to leave it out of the _sign up_ page. This isn't optimal but there are ways around it.

Next, we looked into how we would setup the sign up flow for this test app. There are many ways to do this and each way has it's own challenges and solutions. We chose a classic method of not immediately signing in a user.

As a consequence of this flow, we were able to leave `NextAuth` out of the _sign up_ flow. This in turn meant we could use `useFormState` combined with a server action. This is great because it made our job easier.

The last part we handled was setting up Types for `useFormState` which can be a bit complex and requires some effort.

Next step will be sending emails and setting up an email verification page.

---

If you want to support my writing, you can [donate with paypal](https://www.paypal.com/donate/?hosted_button_id=4D78YQU4V5NEJ).