๐Building Accessible Forms in Next.js with Server Actions ๐คฏ
Add Forms to Next.js the Right Way with Server Actions
Hey everyone, it's your friend TAQUI here! In this article, we will look at how to build effective and accessible forms in Next.js applications using Server Actions. We will cover handling form submissions, data mutations, loading states, errors, and more - directly within React components for a unified and seamless experience.
Intro to Forms in Next.js
Traditionally in Next.js, forms were handled by creating separate API routes to handle submissions on the server-side securely. This required making additional requests to the backend after submitting a form.
While this works, it has some downsides:
Multiple network requests are needed - one for submission, another for the response
Form handling is split between the component and API route files
It's not straightforward to integrate loading/error states
Server Actions were introduced to address these issues by allowing data operations and business logic to be directly integrated into components.
Server Actions Overview
Server Actions allow defining functions that securely run only on the server-side. These can be called directly from React components using the action
prop on forms.
Some key capabilities include:
Capability | Description |
Data Validation | Validate form data against schemas before handling submission |
Data Operations | Perform CRUD actions like inserts, updates, deletes directly in database |
Error Handling | Catch errors from data layer and surface in UI |
Revalidation | Call revalidatePath() to refresh cached data/UI after mutations |
By handling everything in the component, forms can provide a unified progressive enhancement experience whether JavaScript is enabled or not.
The Example App
To demonstrate these techniques, we will build a simple Todo list app that allows adding and deleting items.
The core pieces are:
Displaying existing todos from cached data
An "Add Todo" form component
A "Delete Todo" form for each todo
Loading/error states with React hooks
Ensuring accessibility
Let's dive into each part!
Displaying Todos ๐
We render the initial todos data from the server component:
// pages/index.js
export async function getServerSideProps() {
const todos = await fetchTodos()
return {
props: {
todos
}
}
}
function Home({todos}) {
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
)
}
Adding Todos
The Add Todo form component renders an input and submit button. It specifies the server action handler:
<form action={createTodo}>
<input />
<button>
Add Todo
</button>
</form>
The createTodo
action imports Zod to validate the data schema:
// serverActions.js
import { z } from 'zod'
const schema = z.object({
title: z.string()
})
export async function createTodo(req, res) {
try {
await schema.parseAsync(req.body)
// insert into db
await db.todo.create(req.body)
return res.send({msg: 'Todo added'})
} catch (err) {
return res.send({error: err.message})
}
}
On success, it calls revalidatePath()
to refresh the page cache. Now submissions are handled securely on the server without separate requests.
Deleting Todos
Deleting follows a similar flow. Each todo item renders a delete form passing the ID:
function TodoItem({todo}) {
return (
<li>
{todo.title}
<form action={deleteTodo}>
<input type="hidden" value={todo.id} />
<button>Delete</button>
</form>
</li>
)
}
The deleteTodo
action removes it from the database and revalidates.
Loading and Error States
The useFormStatus
hook from React exposes pending state to toggle loading styles:
// SubmitButton.js
function SubmitButton() {
const {pending} = useFormStatus()
return <button disabled={pending}>Submit</button>
}
Errors are surfaced by returning messages from failed server actions.
Accessible Design Patterns
Some strategies to ensure forms are accessible include:
Labeling all inputs
Announcing success/errors to screen readers
Avoiding interruptions with a "live region"
Using ARIA attributes like
aria-disabled
See the full example on GitHub for the implementation.
Conclusion
By integrating forms fully into components using Server Actions, Next.js provides a clear and unified approach. Submissions are handled securely and directly in React without separate requests. This simplifies loading states, errors and overall UX.
With some best practices around validation, navigation and accessibility - Server Actions make building effective forms straightforward and progressive.
๐ก If you find this article helpful then don't forgot follow me in Github and Twitter .
Like ๐
Share๐
Follow me in Hashnode โ