Intermediate Next.js

Auth Actions

Scott Moss

Scott Moss

Superfilter AI
Intermediate Next.js

Check out a free preview of the full Intermediate Next.js course

The "Auth Actions" Lesson is part of the full, Intermediate Next.js course featured in this preview video. Here's what you'd learn in this lesson:

Scott creates server actions to handle authentication from the sign-up and sign-in forms. The actions are created in an actions directory outside the app directory. The registerUser action is then imported into the SignupForm component.


Transcript from the "Auth Actions" Lesson

>> Scott Moss: But we're just gonna hop into it, cuz that's the fun part. So if you click on implementation, basically implementation is all the code we're about to write for this. What we need to do, the goal is to, if you saw when I did the demo of the app, I had the sign in form, the sign up form.

We need to make that work. It doesn't do anything until we hook this up. So first what you need to do is, you need to check out to a different branch. You check out to the step/0 branch. This branch has, it doesn't work. There's nothing in here, [LAUGH] there's only the bare minimum in here.

Our goal is to get this to work. Right, we're gonna go in and start making these things work together. The main branch is where everything is completed. So that's what I showed you at the beginning. But we're gonna start on step 0 with pretty much everything stripped out, and we need to go in and enable it.

The first thing we're gonna do is getting those authentication forms to work using form actions. So the first thing we need to do is, so again, I'm gonna tell you what my opinion of it is while also telling you what I think. NextJS want you to do is not, [LAUGH] they don't always match.

What I like to do with my actions is, I like to put them in an actions folder on the route. There's many ways to make actions. You can make them directly inside of a server component and different things like that. I prefer to put them on the routes.

You can even make them inside of client components. When you do that, it's just really hard to test them, move them around, just like any other piece of code. So I just put them all in a folder. So that's what we're gonna do, we're gonna make a folder called actions.

And for our actions, as far as being able to sign in and sign up, we're gonna make a file called auth. These are gonna be all the actions we use for authentication. So let's do that.
>> Scott Moss: So on the route, not inside the app directory, but literally on the route of the repo, I'm gonna make a new folder called actions.

And inside of there, a new file called auth.ts. So yeah, please follow along if you can.
>> Scott Moss: And then from there, there are a few imports, get back to my notes, that we wanna use. Talk about the use server directive and why we need this outside of a component, right?

So use server. Anybody know what this does? This directive does, or wanna guess?
>> Speaker 2: Or says that this page should be processed on the server.
>> Scott Moss: Yeah, you would think so, [LAUGH]. There's actually.
>> Speaker 2: Okay.
>> Scott Moss: There's actually a, where is it? Server only, there we go. That's what this does.

Yeah, that makes sense. Use server should force it, right? Yeah, this use server is a hint to the React compiler that this file should be ran in a NodeJS environment, basically. It's like, this should be ran in a NodeJS environment. It also hints at how, when you start composing components together like a client component with a server component, it hints to the compiler that, these things can't belong in this order.

You can't import a server component into a client component, or something like that. So it gives the compiler hints and it tells it what environment it should run in and what it has access to. Also, just from a human perspective, it's just something you can look at and be like, okay, I know this file has the context of a server, so therefore we're good.

But in the case of Next.js and React, you have to add this. You cannot use an action, a server action without that server action being scoped inside of a use server directive. Either globally in the page or in a component, you could put use server at the top of the function you're making, which I think is a gross UI and you should pretty much always do this, in my opinion.

I don't know why they even show you the other one, but you can do it. As far as these imports, cookies, nothing special here. I will talk about how this is used because it's kinda cool. I already have some pre-made signin, signup functions. You can go check those out, we won't be working on those.

It's just being able to take a email, password, hash it, sign in, take the email password, hash it, sign up, that type of thing. Typical email password sign up, nothing crazy. Zod, we're gonna use this for schema validation so we can validate our inputs. Redirect, so we can redirect to the dashboard once someone signs in or signs up, and the name of the cookie that we'll be setting.

So what we need to do, the objective, is we need to create a function in which we can call when someone completes the sign up form. We're gonna call that registerUser. And we need to create a function which we can call when someone creates the sign in form, we'll call that signinUser.

So let's do that. So for registerUser, we go back to my code. Let's do that. I'll say, const registerUser equals async. And I'll talk about these arguments. So the first argument is gonna be the previous state of the form that this action is gonna be bound to. That's gonna make more sense when we go make the JSX.

But we're gonna be using this special hook that basically provides the previous state of the form as the first argument into which the action that you give it is part of this special hook that we're gonna be using. So we won't be using this example, but that's always gonna be the first argument.

And then the second argument is actually gonna be the form data, which is gonna be type formData. If you don't know what formData type is, it's just a map. It's a map of all the input fields by the name that you gave them in the form.
>> Scott Moss: So we have that.

Like always, never trust a client. Probably wanna validate these inputs before you do anything. So I'm using Zod here to make a schema for that, so I just created a schema. If you don't know what Zod is, it's just a runtime schema validation library. You can create a schema, pass it an object, and try to validate it.

It'll throw an error if it doesn't match, it'll succeed if it does. It's a very popular schema validation tool. So we have our authSchema here. It's just checking to make sure that this object has a email and it has a password, and they're both type string. So once we do that, what we wanna do is we want to try to validate that schema.

We can do the parse function. By default, this is gonna throw an error. If it fails, that's fine, we'll talk about how we might wanna handle that. So let's do that. We'll say, data = authSchema.parse. And we wanna pass in the email here. That's gonna be formData.get because it's a map, and the field on that form is gonna be called email.

And then for password, it's the same thing, just password. You have to call the get method on maps.
>> Scott Moss: Okay, assuming that's good, we're gonna wrap the next part in a try-catch, the actual sign up part. And the reason we wanna do that is so we can show a message on the form if the sign up fails.

So we're gonna try-catch that. So let's do that. So we'll say, let's try and get a token from awaiting the signup function that's imported at the top. This just takes in data. It takes in an object with the email and password, which is what this schema returns if it's valid, cuz it's email password.

So that's that object. It returns an object with the user and a token. We're just gonna destructure and get the token.
>> Scott Moss: And then if it succeeds, what we need to do is, we're using JSON Web Tokens for authentication, and I'm just gonna set them in the cookie.

The reason I'm setting them in the cookie is because I want to be able to access the JSON Web Token on all of my server-side calls and functions which have access to the cookies. If I store the JSON Web Token in a local storage or something like that, I would not be able to access that JSON Web Token server-side.

There's no way to access local storage from a server. And because we're not sending any requests on a lot of these things, I can't just take it out the local storage and send it to the server. It's possible, it's just a lot more work. I just inflate doing it.

Honestly, you should be storing JSON Web Tokens in secure cookies anyway. You should be storing them in local store. So we need to set it, you can use the cookies. We're able to use cookies here on the server because, again, we didn't make the request. But essentially what's happening here is a request.

So just like any request handler, we have access to the request. So by just importing the cookies, it already knows what current request is happening right now, so we don't have to tell it. So we can just call cookies and it knows what to do. So in this case, I'm just gonna say cookies.set, the cookie name with the token.

And then if there's any error, just log the error, and then we're gonna send back a message. So let's do that. So we'll log the error, we'll send back a message, fill to sign up. And at the end, if everything works, we'll do a redirect to /dashboard. You might be asking why I don't just put the redirect here and the try after here.

There is a bug for some reason on this redirect. I had to go deep into the Next.js issues to figure this out. If you put this in a try-catch, it actually catches an error somehow. [LAUGH] I guess there's an error being thrown here, even though it works. And that your try-catch will catch it and it will land here, it won't actually redirect.

So don't put a redirect in a try-catch. I don't know if that's a bug, if that's intentional. But yeah, I was stuck on this for two days, about three months ago, and that's what I found out. Don't put it in a try-catch.
>> Scott Moss: Cool, so we have that.

Export this, registerUser. Now we want to hook this up to the form. So let's do that. We go back to our notes. Couple things we need to do is, one, we're gonna create a Submit button. I'll talk about why the button is separate from the rest of the form.

And then we need to add all that stuff to the form, which should be mostly built out, I think. I can't remember what stale left it in, but let's go check it out. So if we go to app, you should see a auth folder for the season it.

Quick refresher, parentheses around a folder in Next.js app directory means this route segment won't have a route. There won't be a slash auth, but you want a folder because you want all these child segments to share a layout. So it's like a group. It's called a route group.

It's like, I wanna group all these routes together so they can share this layout, but I don't wanna make a new route segment called auth. So in this case, I can still go to slash signup and slash signin. Whereas if I didn't have these parentheses to be slash auth signup, slash auth signin, which is gross.

So I put this parentheses here, which allows me to not have that in the route segment, but still get the benefit of sharing a layout here because they're basically the same thing. So that's what that is. Okay, so let's go to, actually we're gonna go to the components.

We're gonna go to signupForm, and this is where we wanna do our work. So in the signupForm, notice, it's a client component. This is important, this is very important. This is a client component. You can import server actions and client components. That's the whole point of them, actually.

Because most of the time, if you think about it, you probably wanna call some action on some user interaction, on a click, on a submit, on something a user did. You can think of it as the same case where you would have made a post request, you would have made any type of request when a user did something.

Okay, replace that request with a server action. Now I wanna do a server action when a user does something. Right now we're specifically talking about forms, but later we'll talk about how you can use server actions on any interaction, not just forms.
>> Scott Moss: Okay, any questions on that?

I think the Next.js docs just over explains that, but it's really just a function that gets executed on a server, but you can call it on the client. The best way I can describe it is, imagine if you had some code gin, whereas if you made a functional server, you run this script, and it automatically creates a route for you and abstracts that route away and a function that you can call anywhere.

That's the best way I can describe it. It's just an abstracted route that you didn't have to configure yourself. Okay, cool, so first thing is, we need to useFormState hook to set up our state. And then we need to bind that to our form, and then we can kinda use the message here to show an error message, and we'll go from there.

So first what we want is to useFormState. Notice, that comes from react-dom. That does not come from Next.js. That is a React thing. So we got that. We're gonna create an initial state. Since we're only passing a message, we're just gonna have our initial state just be a null message like this.

I'm gonna create our hook, useFormState. useFormState takes in two arguments. It's gonna take in the actual action you want to run when the form is submitted as the first argument, and then the initial state as the second argument. So the action we wanna run is gonna be the registerUser one that we just made.

So I'm gonna say registerUser. And then the initial state is the initial state here. My TypeScript is freaking out. This thing freaks out if you don't give it a generic. You don't have to, it's just TypeScript, but if you want to, I do have it in the notes.

You basically just type out the initial state if you wanna do that. I'll just do it right now for all the TypeScript stamps out there. So message would be string or null.
>> Scott Moss: This is just gonna freak out, don't worry about that. And then here what it returns is gonna be a tuple, like most hooks in React.

The first one's gonna be the actual form state. In this case, it's gonna be a object with a message on it. And then the action that you can call, in this case we're gonna bind it to the form action. So let's do that. So we have our formState, and then we have our action.

>> Scott Moss: And then on the form, it's not on submit. No longer are you doing on submit. On submit is a client-side thing. A form action, if you're OG, you typically give it a URL, right? You'd be like, I wanna go to this route here, .php, or something, right?

You do something like that, right? That's OG right there. So it's the same thing, except for it's like, well, I just wanna run this action, which is, again, it's like a concealed route. It's like a route that was made for you, concealed as a function. So it's very similar to just putting a route there.

You just don't know what that route is, it's a generator route.
>> Scott Moss: Any question so far?
>> Speaker 2: I do have one question. Out of curiosity, I know this is a client component.
>> Scott Moss: Yes.
>> Speaker 2: And I'm assuming because some of the components that we're importing are also client components.

But I'm just curious, would it be possible to maybe duplicate this form purely as a server component?
>> Scott Moss: If you replicate this form as a server component, then you couldn't handle any of the interactions, because interactions are only possible on the client. So as soon as you go server component, you have no interactions.

>> Speaker 2: So server components are typically read-only.
>> Scott Moss: Yeah, so a forum, I would imagine if you added a forum, you want some type of interaction. So yeah, it wouldn't be a simple way to do that.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now