Let's take a look at how you
can create forms and handle data mutations with the Next.js
App Router and Server Actions. Before we get started, let's see what
we'll build. So I have a pretty simple form where I can of course add some items and
delete them as well, and while it's simple, we'll talk about some of the common
patterns you'll need like loading states, and error states, and also making
your forms progressively enhanced. But first, before we get into the App Router, let's talk about how you might have been building
forms today. So on the left, in my editor here, I have an example of a form in the Pages Router.
Now, you'll notice we have the familiar form tag and event handler for onSubmit, an input, and
then some button. And you click to submit this form it's going to call your event handler
and we have to call a separate API route. So traditionally, you would create an API
Route in your application, you would talk to the server securely inside of that API route,
since this code and this event handler would run in the browser, and then you could get
the response back and do something with it. This is how it's worked in the Rages Router
since Next.js was originally created, but today we're going to talk about how to handle
things a bit differently with the App Router. So I went ahead and created a new
Next.js application to get started using create-next-app, you can do `npx
create-next-app@latest -e next-forms`, which is the name of this specific one to
get all the code that I'm showing today. So we have a basic root layout here, and
then the entry point into our application, which is the page file that marks the
entry route. Now, inside of this file, this is a server component in the
App Router and it's asynchronous, so we can fetch data directly to display the
todos on the page, so it's marked as async. We grab the todos back from really anywhere,
in this case we're using Vercel Postgres, but could be any service or any promise,
and we take those todos and we map over them inside of a list and then we
also add a form for adding todo. So some basic scaffolding here to get our
application ready. Let's take a look at adding a todo. Okay, so let's look at the add form
component. Now, there's some things in here that might look familiar, and also some new things,
so starting on line 25 we have the form tag, but you'll notice instead of using an event
handler like onSubmit, we have the action prop. Now, for those who are familiar with
HTML, this might look familiar. The action prop is used for processing a
form, and in traditional applications, that action is some URL that it sends the form
data to. Now, this is great because it allows you to really easily send over your data and
it works if you have JavaScript disabled. But what if you want to have more than one form on a
page? What if you want 10 forms on a page? Well, the cool thing about server actions is
it's going to make this really easy to do, plus some other benefits that we're going to
get into. So this action, which is actually just calling a function that runs on the server,
which means there was no separate API that we had to set up, there was no API Route or
no Route Handler, we just call functions. We of course have an input, as well
a submit button, and then also a live area that we're going to talk about
a bit later around accessibility. So, we have this form action that takes in a server
action called createTodo, and then it also has some initial state. This initial state is just
a message that we want to return from our server action whether it was successful or it failed,
but let's jump into this specific create todo. So this file contains all of the server
actions for our applications denoted by the "use server" directive at the top of the
file. That means that everything in this file runs securely on the server and does not
get sent to the client. So specifically, we have our createTodo server action
that takes in the previous state from our useFormState (we'll get to that in
a second) and then also the form data. Now, let me step through each part of
the server action, how it's happening, and then I'll show an example of how it works. So first we have the form data. We're using
Zod to validate the data that we're getting back is correct, so we're expecting a string and
we're expecting it to be there. You could use whatever library you want you here, but this
is just an example showing how we're parsing that schema and then getting the data back. Then
inside of a try catch, we call Vercel Postgres, or we call our Postgres database, to
insert this new value into our TODOs table. Then, we revalidate the data. So this is going
to go to the index of our application it's going to look at the data that's been cached
when we fetched it back from the server, it's going to say you know what we just did a
data mutation, so revalidate all of the data on that path, and then it's going to return back a
message saying that it successfully added todo. Okay, so what does this look like?
Well, let me go over to the browser on the right and I'll add a new todo that
says new todo, hit enter to submit my form, and we can see it was successfully
added here. And this works whether we have JavaScript enabled or disabled, which
can show how progressive enhancement works. So let me just disable JavaScript here and I can say "another one" and add it and we can see that this works as expected here. Let me just re-enable this and this works for adding or deleting todos as well too. Now, you might have noticed
that when we submitted the todo, it put the add button or the submit button
into a loading state as we can see here. Let's take a look at what that
looks like. So back in our add form, we have this submit button component and
it's pretty simple. It reads from a new React hook called useFormStatus, it uses the
pending boolean to determine whether it should put this button into a disabled visual
state, and that's really all it takes. Now, it's important to denote that there's
been a couple hooks that we've been using, useFormStatus and useFormState, that are
coming from React. These are new hooks that are in react canary, which is a channel
that's made available for frameworks to adopt and put the latest React features out into the
world that will then come to a stable general purpose React channel for the rest
of other standalone React users. I'll include a link down in the
description if you want to check out how to use these hooks in standalone
React on the canary channel as well too. It's a pretty similar story for deleting
todos as well too, so if I go back to my page, I iterate over the todos and then I have
a delete form for each of the individual todos that has the ID in the text. So if I
click in here again, it looks pretty similar. We're using form state to take in both the
action that we want as well as the initial state again. This is just some message for
either an error state or a success state. We have some hidden values in our form
that allows us to pass additional data to the form data for our server action. We have
a delete button that looks very similar to before and then we have our live area as well too. So what does this look like? Well, I'll say
another one I'll add it and I can click delete. Now, as I mentioned before, this allows us
to have "n" number of forms on our page. We can have a bunch of different forms and
critically one really cool thing that Next.js and Server Actions provide here is the data that can
be returned from the server actions can not only update the cache data, it can also provide the new UI in one network roundtrip. That's pretty cool. The Next.js App Router architecture considers all of these individual pieces like fetching, caching, and revalidating data, and integrates
them all together. So for example, when we're deleting a todo, just like in
creating, we're able to call revalidatePath to update that cached data and as you see here
we click delete, it makes a network request, but then in that network request in that round
trip we're able to say you know, what we made a mutation on our data and we have some new UI
to show by updating what values were cached. Now you might be thinking, that's great but
what if I need more fine-grained control over revalidating the data in my application,
more than just an entire path? Well, you can also use revalidateTag for
specific data that you've tagged with the cache tag and then if you're not
using the fetch API we are soon releasing a stable version of a way to tag any data as
a function through cache() that you can both set revalidation times as well as cache
tags on, so stay tuned for that as well. Let's also talk about accessibility. So a couple
things I want to point out in this application, one inside of this form, we've ensured
that our inputs are correctly labeled for screen readers. Two, we have this live area
that analyzes changes in our application, so I have voice over on right now on Mac.
Let's say I want to add a new task here [voiceover] "I am currently on the text
field to enter text. To enter text..." [Lee] and I hit enter [voiceover] "added todo hello" [Lee] and we see that it says that I added this todo hello. Now that's being read out in the live area here that's politely not
interrupting my screen reader if something more urgent came in and it's not being shown, so I've
added a little CSS here so it's only for screen readers and you'll notice that, let me just close
this for a second, if I go back to my action the value that is returned from the server action
that is used by useFormState is what is put into that live area that's announced, so either
we've successfully added a todo in the create case or in the delete case or we've failed to
create or delete a todo. So to quickly show what this would look like in an error state, what
I've done is just put a delay of two seconds and then I've early returned to show that there was
an error state. I could also throw an error here as well too, so let me just type in test. I'll hit
submit. [voiceover] "you are currently on the – failed to create todo" [Lee] and we see that it says failed to create todo which is returned from our server action if there was some issue saving to your database, for example. One other small accessibility bit here is that we're using the aria-disabled attribute
on our button rather than disabled because for some screen readers, that will actually hide the
button if disabled is true, so this is a little bit better for the use case of this example. Now
there's a whole bunch more that we can do with forms in Next.js and with Server Actions in react, we can read cookies we can set cookies we can look at headers we can handle optimistic UI, we can
redirect–there's a bunch of stuff but for the sake of the video today, we'll keep it just to
some of the basics. The docs for this are live on nextjs.org, as well as this example, which
I'll link in the description as well too. Let me know if you want to see more videos like
this and stay tuned for the next one. Peace!