Getting Started with Next.js
I'm going to try to teach you as much as I can in five minutes about Next.js. First step is don't read the docs. Let's just go over here to a terminal and say npx create-next-app@latest and we'll call it my-app. Just go ahead and spam the enter key and make an application. Go ahead and wait a couple minutes for npm to finish installing all of its packages. Let's open up this app directory. Let's go ahead and enter that app directory and say npm run dev. Go ahead and click on Localhost 3000 in the terminal and you'll see that the Next.js app is running.
Understanding File-Based Routing
So the first important thing to understand about Next.js is that it uses a file-based router. So inside of src/app, you have these page.tsx files, and these are used to declare what is going to be on your page. In our case, let's just get rid of all this extra boilerplate because we don't need it, and I'll just say "Hello World" and just verify this shows up.
So let's make a new route. I'm going to call it users and we're going to go ahead and just make a new page.tsx here. Let's copy this page and I want to show you that you can make a new page as simple as this. Say "users page." So now we can go to the top route and just say /users and notice that that new page shows up.
Fetching Data in Server Components
So now let's learn about data fetching. So typically you have a data access directory, and I'm going to go ahead and just say put a users file in here. I have some code that basically just stores and retrieves from a global storage so that when Next is restarting, I don't lose this data. But we have two methods, getUser and updateUser. Let's try to use them in our React Server Component.
So when a page loads, often you want to fetch data. So how do you do that? Well, put async in front of this. I can say const user = getUser like this, but we need to pass it an ID. The way you can get the ID is you can actually add another folder here called [userID] and this allows you to get path parameters. So if I go up here and say params, I can simply say params.userID.
This is complaining because we haven't typed this, so let's just go ahead and add some TypeScript types here so that we know that a userID should be passed over in the path parameters. And remember to await on any promises, and we can actually just display this name right here. If we go back to the app and now type in like 50, we should see "users John Doe." Now the key thing to point out is that this technically is coming from an external data source if you're using like a database. So that's how you can actually fetch data when your React Server Component is running through.
Mutating Data with Inline Server Actions
But let's kind of take it to the next step. How do you store data? Let's make a form here and I'll give it an action. And inside this action, you can actually just give it a "use server" anonymous function, and then we can go ahead and put an input here so we can keep track of the name, and then also a button for submitting it. So I'll say button and then we'll say submit.
Now by default when this form submits, if you're using an action like this, you'll get some form data. So I'll say formData, and then we're going to say const newName = this. And then we're going to update that name. Say updateUser again, which is a method that came from that data access file. And we want to basically update the name where the user ID is equal to that. And then we'll put async in front of this like that.
Let's just style this button real quick as well, and then also style this input. Okay, let's just type in "testing" and click submit. Notice that the app doesn't by default update this, so you actually have to manually refresh it. Now the way you can tell Next.js to clear its caching mechanisms and refresh the page is simply after you modify some type of data in your database, just call revalidatePath and then pass it the path that you want to refresh basically for your user. So now if I were to type in "hello world," click submit, notice that that automatically updates. Pretty cool.
Refactoring to a Dedicated Action File
But let's take this a step further. Let's say you wanted to basically show a spinner on the button when you try to submit this. How would you do that? Well, typically what I do in my applications is you can make a new file here called actions. You can put "use server" at the top. This is a string that you'll need to tell Next that this is going to be basically a bunch of API endpoints you can invoke. And we're going to make a function here, I'll say export async function updateNameAction like this. It'll take in some formData and we can go ahead and delete some stuff. Let's just go ahead and auto-import those things as well. And we're going to keep the revalidatePath, but now we don't know what the user ID is, because before we could just pass it in.
So there's two ways to achieve this. One quick way is say updateNameAction.bind() and then pass it the ID of the user. And if you go up here, you should get a user ID of a string, and then you can actually use that user ID in both locations. Let's try that out. I'm going to say "hello world!", click submit, and that still works.
But there's actually a hook built into Next.js that really helps you out here. So I'm going to go ahead and say const [state, action] = useFormState. Go ahead and auto-import that. And we want to pass in the updateNameAction we have, and then I'm also going to pass in a user ID like this. So now we can actually just call this action inside this form submission.
And then go over here and this will be previousState and this will take a userID of a string. And we can just simply return the same user ID. And of course we'll get that out of the previous state. So that's a way you can kind of initialize the action with some data. You can also put a hidden input and just submit the user ID that way, but this is just one approach that works pretty well.
So unfortunately, you can't use useFormState inside of a server action. So what you typically have to do is make a new file called like form.tsx. Then we're going to bring in our entire form, but we need to put "use client" at the top. If you want to use a custom React hook, make sure you have "use client" at the top. And we'll call this just a form. And let's just get rid of some of the stuff that we don't need. Now we do need the user ID, so let's just go ahead and bring in a user ID like this. Now we will need a user ID in the props. So let's just go ahead and say userID as a string. And we can start using it places. We don't need to fetch the user anymore. And we also can't use an asynchronous keyword inside of a use client component.
Now the last step is we really just want to export the form. We don't want all this extra stuff. So now we have a form that's in a nice file by itself and we can actually use that down here. So let's just go and import our custom form like this. Make sure we pass it in a user ID, so user.id like this. And then finally, let's clean up that useFormState hook because you cannot use hooks in React Server Components. So let's go ahead and save that. Go back to our app, testing, submit that, and that all works.
Now the reason we're doing this is because we want to show a little spinner inside the submit button. Luckily, there's another hook called useFormStatus which if we were to extract this button into its own special button, so I'll just say const function submitButton, we can go ahead and return that button like this. We'll call submitButton here.
So now that we pulled that button out, we can simply say const status = useFormStatus(), and that's going to have a pending flag on it. So I can say status.pending and if it is pending we'll show 'Saving...', otherwise we'll just show 'Save'.
So let's just try this out. Go ahead and just type in some information, click save, and notice that it shows that quick loading state over here. Now to kind of add a fake delay, let's just go ahead and say sleep(1000) and then add a promise that just takes some time. And I'll say "testing again," click submit, and notice that it does say 'Saving...'
Now there's one last issue I want to point out is that basically when the form submits, sometimes you need to clear out the input. The one way you can achieve this is just add a ref to this form. Go ahead and bring in useRef and I'm going to pass that to the form itself. And if the state returns a say message: 'success', which we'll default it to nothing for right now, but over here we actually say message: 'success', we can just go ahead and clear out the form. So I'm going to say formRef.current.reset(). Let's test it out. I'll say "hello world," click enter, and notice that it clears out the form once a success message comes back.
Recap of Core Next.js Concepts
Now I will say there are probably tons of other Next.js features you should read about in the docs, but overall this is the pattern that you're going to be following a lot when you're building out an application, which includes the file-based routing, the path parameters, creating a page, creating a use client for forms and having a loader on those forms, using useFormState, how to clear out the forms after they're done submitting, how to create an action and have that return some data and revalidate the current path the user is on, and also how to mutate data in your potential database.
So let me know if you like this really short, compact video of me trying to teach something, and I might do it again. Otherwise, have a good day and happy coding.