How to Implement an Account Registration Screen Using Server-Side Validation (Next.js & Zod)

  • nextjs
    nextjs
Published on 2024/02/01

Introduction

Up to the previous article, we used React Hook Form and Zod—libraries that provide the necessary features for input forms—to build an “input form” and implemented it so that when the “Create account” button is clicked, the input values can be checked in the console log.
This time, we’ll implement the process of passing the input values to a server action. Ultimately, we’ll register the data in a database, but since the process is a bit complex, the explanation will be split into two articles.

https://shinagawa-web.com/en/blogs/react-hook-form-validation-with-nextjs

Goal of This Article

The goal of this article is to input a name, email address, and password on the account registration screen.
After that, we’ll verify the input values in a server action and, if an error occurs, implement a process that returns an error notification to the input screen.

Image from Gyazo

Creating the Server Action

First, create a new actions folder and then create a register.ts file.

register.ts
'use server'

import { z } from 'zod'
import { RegisterSchema } from '@/schema'

export const register = async (values: z.infer<typeof RegisterSchema>) => {
  const validatedFields = RegisterSchema.safeParse(values)
  console.log(validatedFields.data)

  if (!validatedFields.success) {
    return { error: 'Please correct your input' }
  }
  return { error: 'An error occurred' }
}

By defining 'use server', this function will be called on the server side.
We use Zod, which we introduced in the previous article, to validate the received data.
One of the convenient aspects of Zod is that it can be used in the same way on both the client side and the server side.

We validate the received values, and if an error occurs,
we can return an error message to the client that called the action.

In this case, since we're applying the same validation on the client side, in principle the message Please correct your input should almost never be returned.

So we can basically ignore this branch for now and return a message to the user in the final return.
For testing purposes, with the current settings this server action will return an error 100% of the time (we’ll fix this later).

Configuring the Client Side to Call the Server Action

Now let’s modify the account registration screen so that it calls the server action we just created.

register-form.tsx
+ import { register } from '@/actions/register'

(※some parts omitted)

- const onSubmit = (values: z.infer<typeof RegisterSchema>) => {
+ const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
+   const response = await register(values)
-   console.log(values)
+   console.log(response)
}

Since the server action is set up as an asynchronous process, don’t forget to add async to onSubmit when calling the server action.
After calling the server action, we check the response in the console log.

When you fill out the account registration form and click the button, the response from the server action will be displayed on the client side,

Image from Gyazo

and on the server side you should see the contents of the input form.

Image from Gyazo

If you see output like this, you’re good to go.
We’ve successfully passed the contents of the input form to the server side.

Notifying the User of the Result of the Server Action

Right now, the result of the server action is only shown in the console log, so we’ll display it in the input form to notify the user whether account creation succeeded or failed.
We’ll implement this in three main steps:

  • ① Create notification components
  • ② Add the components to the input form
  • ③ Update onSubmit based on the server action response to display the notification components

① Create Notification Components

We’ll create separate components for when the server action succeeds and when it fails.

form-success.tsx
import { CheckCircledIcon } from '@radix-ui/react-icons'

interface FormSuccessProps {
  message?: string
}

export const FormSuccess = ({ message }: FormSuccessProps) => {
  if (!message) return null

  return (
    <div className="flex items-center gap-x-2 rounded-md bg-emerald-500/15 p-3 text-sm text-emerald-500">
      <CheckCircledIcon className="size-4" />
      <p>{message}</p>
    </div>
  )
}
form-error.tsx
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'

interface FormErrorProps {
  message?: string
}

export const FormError = ({ message }: FormErrorProps) => {
  if (!message) return null

  return (
    <div className="flex items-center gap-x-2 rounded-md bg-destructive/15 p-3 text-sm text-destructive">
      <ExclamationTriangleIcon className="size-4" />
      <p>{message}</p>
    </div>
  )
}

Both components behave basically the same: if they receive a message, they return a component; if there’s no message, they render nothing.
To make things clearer for users, we’re also using icons. @radix-ui/react-icons is the icon set that gets installed together with shadcn when you first set it up.

https://www.radix-ui.com/icons

② Add the Components to the Input Form

Next, we’ll add the components we created to the input form.
We’ll manage the messages included in the server action response using useState.

register-form.tsx
+ import { useState } from 'react'
+ import { FormError } from '../form-error'
+ import { FormSuccess } from '../form-success'

(※some parts omitted)

export const RegisterForm = () => {
+   const [error, setError] = useState<string | undefined>("")
+   const [success, setSuccess] = useState<string | undefined>("")
  const form = useForm<z.infer<typeof RegisterSchema>>({
    resolver: zodResolver(RegisterSchema),
    defaultValues: {
      email: '',
      password: '',
      name: '',
    },
  })

(※some parts omitted)

            />
          </div>
+         <FormError message={error} />
+         <FormSuccess message={success} />
          <Button type="submit" className="w-full">
            Create Account
          </Button>
        </form>

③ Update onSubmit Based on the Server Action Response to Display the Notification Components

Finally, we’ll update onSubmit so that when an error is returned, the error component is displayed in the input form.

register-form.tsx
- import { useState } from 'react'
+ import { useState, useTransition } from 'react'

const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
-   const response = await register(values)
-   console.log(response)
+   setError('')
+   setSuccess('')
+   setTransition(async () => {
+     try {
+       const response = await register(values)
+       if (response.error) {
+         setError(response.error)
+       } else {
+         // setSuccess(response.success)
+       }
+     } catch (e) {
+       setError('An error occurred')
+     }
+   })
}

(※some parts omitted)

-   <Button type="submit" className="w-full">
+   <Button type="submit" className="w-full" disabled={isPending}>
  Create Account
  </Button>

At the very beginning of onSubmit, we reset the messages. Since the previous message content is still there, we clear it and then store new messages based on the result of the server action.

Using React’s useTransition, we disable the “Create account” button until the server action returns a result. This prevents users from accidentally clicking the button twice.

We didn’t implement it this time, but it’s also a good idea to set disabled={isPending} on the Input components as well.

Operation Check

Once again, if you fill out the account registration form and click the button, you should see an error message displayed.

Image from Gyazo

We’ve now successfully called the server action and notified the user of the result.

Conclusion

In this article, we implemented the process of passing input form data to a server action.
We also implemented a mechanism that assumes the process fails, returns the result to the input form, and notifies the user.
In the next article, we’ll implement functionality to register the received data in a database via the server action. At the same time, we’ll update the current server action—which always returns an error—so that it switches the message content based on the result of the database registration process.

Next Article

https://shinagawa-web.com/en/blogs/nextjs-server-actions-db-registration

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form