Next.js 14 Online Store
Video Link: https://youtu.be/lZnUgYTFq6E
GitHub Repository: https://github.com/Ashot72/Next.js-14-Online-Store
This is an online store app that embraces many of the features of Next.js 14.
Figure 1
Next.js employs a file-system based routing mechanism, where URL paths in the browser are determined by files and folders in the codebase. Following
conventions are crucial for proper routing functionality. We create src/app folder and inside the app folder a page.tsx file. This file represents the route.
Figure 2
This page.tsx represents Home page.
Figure 3
Next.js supports dynamic routes. Here we want to access the products of different categories. The square brackets [categoryId] indicates a dynamic route segment.
Figure 4
In this application, we use MongoDB Online https://www.mongodb.com/online with Prisma, a next-generation ORM https://www.prisma.io/
Figure 5
Server actions are asynchronous functions that are executed on the server and can be defined as use server.
Figure 6
Here we retrieve user's order (payment) information via server actions. If you do not specify the user server explicitly, then it is considered a server action.
Figure 7
Although we have not used server action at an inline level in our app, it is possible to use it within an inline function, not necessarily at the module level.
Figure 8
When we start using Next.js, we work within a single project where some of our code is sent down to the browser and executed there, while some remains on the server and is executed there.
In many cases it can be really challenging for us to understand where our code is actually running. You may think that a function gets sent down to user's browser
and is executed there, but that is not actually what happens. Instead, a server action is being executed on the server, so it is staying
on the server.
Figure 9
We will go into detail later on but for now whenever we submit a form, we enter some values, such as a name, description etc. and click on the Save
button. Some JavaScript running inside the browser automatically collects values from the form and assembles them into a packet of data sent off in a POST request to our Next.js server.
Figure 10
Our Next.js server is then going to receive that form data and automatically pass it on to server action. So, our server action again is running
on the server that is why it is called server action. Behind the scenes Next.js is creating a little miniature route handler for us. That is going
to be called whenever a user submits a form.
Figure 11
Note, that form component is a client component and can only import actions that use the module-level use server directive and in this case use server must be define
otherwise, you will receive an error.
Figure 12
Next.js documentation.
Figure 13
Whenever we work on a Next.js application, we are going to build our app out of two kinds of components. First kind is called
Server. Second kind is called Client. The first question that probably comes to your mind is obviously what is the difference?
Figure 14
What is the difference between these two things? A client component is like the same kind of react component you are already used to
using. It is a function that returns some JSX. It is going to be rendered into HTML and then displayed on the screen. A client
component can use all the classic features of react component such as hooks, event handlers, and so on. Server components in the
sense are functions that return JSX, but they have some additional limitations around them, but some additional capabilities as well.
When should we use one or the other? As much as possible we are going to generally prefer to use server components. The reason is very
simple. Server components are very closely integrated into Next.js. If we use server components, we are generally going to get better
performance and better user experience.
Figure 15
The first limitation is that server components cannot use any kind of hooks.
Figure 16
Server components cannot assign any event handlers. So, these are two big restrictions around server components.
Figure 17
As you see, for the client component, we use use client directive. They can use hooks, they can define state, they can set up event handlers
and so on.
Figure 18
One interesting limitation around client components is that they cannot directly show a server component. In other words, if I have a client component, I cannot import a server
component and then show it as a child directly.
Figure 19
Whenever a browser makes a request to our Next.js server, we are going to send them back some HTML immediately.
Let's imagine that we make a request off to our server. Our Next.js server is going to try to render some components.
In this case maybe we have got a single server component that is showing a client component. When this occurs, both
the server component and client component are going to be rendered into HTML.
Figure 20
The resulting HTML will be taken and sent back down to the user's browser. This HTML file that we send down has just plain HTML content
inside of it. There is no JavaScript in there. If we need to reach back to the server to obtain JavaScript code that might implement event handlers or state, the HTML
file will cause our browser to make a second request.
Figure 21
The second request is going to go to the Next.js server. The Next.js server is then going to take a look at all of our different client components.
Figure 22
It is then going to extract all the JavaScript from those client components into a file, and send that file down to the user's browser where it then
gets executed. Even though our client component is called a client component, it still gets rendered one time whenever a user first makes
a request to our server. That just a little bit unexpected sometimes because it is called a client component and you might think that it only
gets executed inside the browser, but that is not the case. It does get rendered one time on the server.
Figure 23
In the header server component, we imported the search-input.tsx client component.
Figure 24
Let's refresh the page and click on localhost. Take a look at what we get in the preview. The fact that we can see that information (search box) in the response
indicates that our client component is indeed being rendered on the server completely. We are rendering the client component on the server and then taking
the resulting HTML, sending it down to the browser, and then a little bit later on, all the appropriate JavaScript from that component gets sent down to the
browser as well.
Figure 25
We need to make sure that whenever a user tries to submit this form, we make sure that they add in some valid name, description etc.
If they do not, we want to show an error message to the user. In addition, we want to make sure we have some general error handling.
If anything goes wrong with inserting data into our database, we want to print out an error message for that as well. This entire process
would usually be very easy if we were making a normal react component. These are forms that can run completely without any JavaScript
in the user's browser. That is the key part. That is why adding the error handling is going to be a little bit challenging. When a user submits the form
by clicking the Save button, information is being taken from the form and sent to a server action. The server action then runs. We may redirect the user
to some other page in our application. We need to somehow allow a server action to run, but allow that server action to somehow get our page
to render again with an error message being displayed on it. In other words, we are trying to communicate from a server action back to a page.
Figure 26
To do so we are going to use a very specially made hook from the react-dom library This hook is called form state.
Because this is a hook, we cannot use it with a server component, so it can only be used with a client component.
Even though we are going to use this hook with a client component it is still going to work just fine even if the user is not
running JavaScript in the browser.
Figure 27
When we call useFormState we pass some initial data, such as objects etc. Whenever we render out form, the form state object is going to kind
of magically embed itself inside of the form's HTML. We can imagine this object kind of gets directly sent down to the user's browser, and it is
going to be included in this form invisibly behind the scenes.
Figure 29
Then a user is going to enter in a name, description etc. and click on Save.
Figure 29
When we submit the form, the browser is going to collect that information. It will be sent of to the backend server where it gets assembled into some
form data object. Along with that form data, the form state object is also going to be included. When our server action gets called, it is going to
receive two arguments. It is going to receive the form state and the form data. Then inside the server action, we can run some validation code and
make sure that the user entered some validated name, description etc. If the server thinks that there is some issue with that data, or if really anything
goes wrong at all, the server action now has the ability to communicate back into our page component.
Figure 30
To add some validation, we use third part package called Zod. To make use of Zod, we are going to import some variable
called Z from the library. Weare going to first create a schema object. This is an essentially a set of validation rules that we want
to apply to some data in our application. We can use a schema object to validate things like arrays, objects, strings, numbers, booleans
just about every piece of data you can imagine inside of JavaScript program.
Figure 31
If we want to pass extra information to a server action, not just form data, we can do it via the JavaScript native bind() function.
Figure 32
In our project, we use NextUI https://nextui.org/, a fully-featured React UI library built on top of Tailwind CSS. You can learn how to integrate NextUI into your project.
Figure 33
We are going to go through the process of setting up Next.Auth https://next-auth.js.org/
Figure 34
We will be using Google Provider.
Figure35
We go to https://console.cloud.google.com/
We should create a new project. I already did but here are the steps.
Figure 35
Giving a project name and click Create.
Figure 37
We open up the navigation menu, go to APIs & Services then OAuth consent screen.
Figure 38
Click Create.
Figure 39
We enter the App name and Email address.
Figure 40
Then we add the authorized domain which is going to be http://localhost:3000
Figure 41
Finally let's go to Credentials and Create Credentials.
Figure 42
We select OAuth client ID menu item.
Figure 43
We choose Applicant type as Web Application. Add Authorized JavaScript origin http://localhost:3000 then add Authorized redirect URI as http://localhost:3000 then click Create.
Figure 44
This process can take about five minutes but you can immediately see the Client ID and Client secret.
Figure 45
We go to .env file create GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET and copy paste the respective values. That is, it!
Figure 46
In the auth.ts file we make use of Google Provider. We need to create app/api/auth/[…nextauth]/route.ts file to handle the requests between
Google Servers and ours.
Figure 47
When we submit a form the server action can take any amount of time and a user is just going to be sitting around wondering what is
going on. It is better to show simple loading spinner.
Figure 48
For that reason, we use useFormStatus hook to show a pending state while a form is being submitted.
Figure 49
Note useFormStatus returns the status for a specific <form>, so it must be defined as a child of the <form> element.
Figure 50
In our app, you may see that we render the same control in different places but with different parameters. On the home page, we show products belonging to the first category.
Figure 51
We show the same products control, but in this case for the selected category.
Figure 52
We use the same control when searching.
Figure 53
You will see that we call query functions in the parent components with different parameters and use them in the child component. This way, we can easily reuse the child component without any modification.
In other words, we Define in Parent, Fetch in Child approach.
Figure 54
Here are the query functions.
Figure 55
In our app, we have a Categories page where we recursively render categories.
Figure 56
To get all categories recursively, we could use the Define in Parent, Fetch in Child approach described above. We will retrieve all categories at the root level, then for each child, we
will find its category, which should work fine.
Figure 57
Our category page.tsx renders CategoryView in CategoryList component. It then calls CategoryView component, which in turn recursively calls itself. We call the fetchCategories() query function,
which is impractical as it will be called for each CategoryView.
Figure 58
Let's print fetch categories in the fetchCtegories function and refresh the category page.
Figure 59
After refreshing the page, we see that we got fetch category printed once.
Figure 60
We used cache(). Let's remove it for a moment.
Figure 61
Now we see that it is called for each CategoryView component making lots of duplicate requests.
Figure 62
Let's see how the caching system works. We will look at the functions that we were calling from our three different components.
Inside of Page we are calling getData with 2 inside Profile and Settings with 1.
Figure 63
These are going to be received inside of the cache.
Figure 64
The caching mechanism is going to take a look at these different functions, and it is going to remove any duplicates that it sees.
In this case the duplicates here are getData(1) and getData(1). Those are absolute duplicates of each other because it is calling the same
function with the exact same arguments.
Figure 65
With the cache, the second instance of getData(1) is not allowed to actually run and make a request
off to the database. Instead, only the unique function calls are allowed to run. In this case getData(2) and getData(1) will actually be executed.
Figure 66
The database will reply and send data for 2 backward to the page and the data for 1 is going to be sent back over to profile.
Figure 67
Settings is still going to get the return value of calling getData(1). The only difference is that the function itself getData(1) does not
actually, be executed. It does not actually make a request off to the database. Instead, it shared whatever response comes back from the
previous call to getData(1). This is the super simple memorization caching system in action.
Figure 68
About cache.
Figure 69
When we run our app, users may see no content whatsoever until all of our different components have been rendered on the server, which is not good. What we would
like to do is render the static parts of the page first, and then render the dynamic parts. On the home page, we have two dynamic parts: categories and products, as it takes
time to retrieve them from the database.
Figure 70
The streaming feature leverages React's Concurrent API and Suspense to suspend or pause rendering until the data is ready and available, leading to a faster
and more optimized performance. The Suspense API provides a fallback UI that appears while the data is loading. This fallback UI is loaded along with other static contents
into the page. This means that as the page loads, the fallback UI is displayed with other contents that are not dynamically rendered. The fallback UI then remains visible
until the asynchronous data fetching is complete. Once data is ready, the fallback UI is replaced with data.
Figure 71
In our case, the fallback UI is the NextUI skeleton control, which is displayed with static data. Once the data, categories, or products are retrieved, they are rendered with
the actual data accordingly. Let's talk about one of the most important and truly surprising things. When developing a Next.js app, we run npm run dev command to run
the app locally for development. For the production build, we run npm run build followed by npm start commands.
Figure 72
Before running the build command, I slightly modified the createOrUpdateCategory function. Once we create a new category, I redirect the user to the home page.
Figure 73
I ran the npm run build command, followed by npm start (or npm run start).
Figure 74
I am creating test category at root level.
Figure 75
Once you click Save, you will be redirected to the Home page and will not see the Test category. Even if you see it once you refresh the page, it will be gone.
When did it go? I can refresh the page as many times as I want and I just do not see it come back.
Figure 76
Actually, I see it created on my MongoDB page. That is a big surprise.
When you start running your application in production mode, the behavior of you application changes in a very significant way.
Figure 77
This all comes back to idea of caching in Next.js. Next has four different caching systems. It is a performance optimization meant to reduce
the amount of work that our application has to do. Some of these caching systems run when we are developing our application. That is the first three.
These three we have been dealing with behind the scenes. When we build our application for production, this last system comes into play. It is called
the full route cache.
Figure 78
We run command npm run build. When we run Next.js is going to find all the different routes in our application. One route is particular the Home page.
Whenever Home page is rendered, we reach into our database, get all of our categories, and then render them out onto the screen. Next.js finds that
route and them asks some questions about it. It does a little bit of analysis and it tries to determine if the route is static or dynamic. We will talk of
these terms in just a moment but right now understand that Next.js is going to assume that our Home page route is static because it is static.
Next.js is going to implement some really aggressive caching on this route. The default behavior is really inappropriate for this kind of application
we are working on right now. We have to configure this or change the caching configuration a little bit.
When we run npm run build it is going to render that Home page component and when it renders that component, we have some code inside there to
reach into our database and get all the categories inside there. Let's imagine we have categories one, two, three. We are going to fetch these.
They are then going to be rendered into an HTML file. This HTML file is then saved to our hard drive. Then, when our application is running in production mode,
whenever someone types into their address bar localhost 3000 and they come to the Home page of our application, they are going to get this exact HTML file
that is going to have the exact same three categories listed in it for all time. Right now, we have no mechanism in our application to update this file in production,
even as we start to edit or add or delete categories. That is why every time we refresh our page, when it is running in production mode, we see the exact
same categories on the screen. The behavior that the category appeared deleted, it was not actually deleted. When we refresh the page, we are just getting
the exact same HTML file that it kind of has a snapshot of our database from when we built our project. We can say, that this default caching behavior is super
inappropriate for our Home page because our Home page has some data that is going to change all the time. That means for lot of applications we work on,
whenever we have a route that is determines to be static, we probably have to configure the caching mechanism where we are going to end up seeing some
stale data when our application is running in production.
Let's focus on what it means to be static or dynamic and how we can kind of figure out which routes in our application are which. Let's run npm run build.
Figure 79
After it is all done, we are going to get that nice, pretty output at the very end. That kind of look like they are just pretty bullet points, but they actually
convey a lot more information.
Figure 80
The symbols we see next to each route tells us what Next.js thinks about each of the different routes we have added into our application. Whenever we
see a little circle like that, that means that Next.js thinks we have a route that contains static data because it is a static route. Dynamic routes are
going to behave a little but closer to how you might expect a traditional web application to work. Whenever someone comes to our application and makes
a request, Next.js is going to render that page and send the result back to that person. Now, the big question becomes how does Next.js decide what a
static route is and what a dynamic route is? All pages by default are static. In order to become a dynamic route, we have to do one of a couple of different
things inside of the route.
Figure 81
The first set of things we can do inside of a page that is going to make it into a dynamic route is to modify cookies or to use anything ties to a query
string, such as access a query string inside of a server component through a prop that is automatically passed to our component. We can also force a
route to be dynamic by setting what is called a route segment configuration. These are very specifically named variables, so we can export from our page
files. Some of different options that we pass to the fetch function to fetch our data is going to turn our route into a dynamic route. And finally, the big one.
This is going to occur in a lot of your applications; any time you use dynamic path. That is a path where you have got one of those wild cards in it, that is
going to automatically, by default, turn this into a dynamic route.
Figure 82
In our case we got the wild card in two routes. You see that some other routes are also dynamic and will come to those a little bit later.
Maybe we want a static route, it is not the worst thing in the world. We do kind of want caching because it does dramatically improve the performance
of our application. At some point in time, even with a static route, we have to accept that our data may change after initially building our application for production.
We need a method for updating this cached file.
Figure 83
Whenever our page is rendering with out-of-date data, we want to see a couple of different ways that we can control caching without disabling
it entirely, but that is of course an option. The first way we can deal with this is we can set up some time-based caching rules where every so many seconds
we will ignore the cached page and we will get fresh new data and render our page. We can set up on demand cache control where at certain points in time,
where we are really sure that data has changed, we can forcibly remove a cache page and the next request that comes in will render it. And the finally, we can
also just completely disable caching is we do not want to deal with the system at all.
Figure 84
With on-demand, at ever certain points in time where we are really sure that maybe the data on some path has changed or the data used by some path has
changed we can call a function called revalidatePath that tells Next.js that we want it to dump any cached version of that particular page, and the next time
someone makes a request to it; it should render the page from scratch. In our application we are one hundred percent aware of when data is changing.
We know our data changes whenever one of our server action functions run. Whenever we create, edit or delete a category, that is a sign that our data
has been changed and it is data that is being used by our Home page. We should probably use that revalidatePath function to tell Next.js that we want
to clear the cache of this page and rebuild it next time someone asks for it.
Figure 85
Let's see how we can enable static caching for dynamic pages by implementing generateStaticParams function.
Figure 86
We will do it for /[categoryId] dynamic path, which is used when you navigate between categories.
Figure 87
Whenever we run the command npm run build once again, Next.js is going to find all the different routes in the application. One it is going to find is
/[categoryId]. Next.js is going to mark this route as dynamic but we can still get some caching enabled for this page by implementing a function called
generateStaticParams. This is going to be a function that we export from the page file. Inside the function we are going to write out some code that is
going to reach into our database and get all of the current categories that exists inside there. Then from the function we are going to return an array
of objects that each have Id that is going to be the Id of each of these categories in our database. Once these different URLs have been generated,
Next.js then is going to prerender these different routes. Each of this are going to render at build time. They are going to rich into the database,
get those categories, pull them out, render the page, and then Next.js is going to cache the result.
Figure 88
We implemented the generateStaticParams function based on what we just described.
Figure 89
After running npm run build command that the built output is a little bit different this time. It is telling us that it has found /[categoryId]
and generated static params.
Figure 90
These are categoryIds found in our database. Next.js made the cached version of those Ids.
Figure 91
When you run the app in development mode with npm run dev and navigate between categories, you will see loading, then categories and products rendering. Categories are not cached.
Figure 92
In production mode, switching between categories is super-fast. Both categories and products render instantly since we are viewing the cached version of the page, with no delay. The only problem
here is that we have cached pages, which means that now we are back in that same scenario, we were before.
Figure 93
We are back to on-demand cache control, but with some additions. When we create a new category, we should not only revalidate the path for the Home page
but also, for the remaining categories since the path is not dynamic anymore. For example, if you create a new category, let's say test, then when you return to
the home page and try to navigate to another category, such as Lego, the newly created test category will disappear. You will get the cached version as
the path for that category has not been revalidated.
Figure 94
Creating test category.
Figure 95
When navigating to any category, all pages are cached, loading instantly, and the test category does not disappear when navigating to any category.
Figure 96
In our application we authenticate a user both on client and server components.
Figure 97
When we sign in, we authenticate a user using the client-side control header-auth.tsx. However, when a user wishes to view their payments, we check whether they are authenticated
or not using the server-side component. Both processes work in the same way; we retrieve the same properties whether we call authentication on a client control or server.
Figure 98
In a Next.js app, we should prefer using server components instead of client ones. Previously, we used the header-auth.tsx file as a client component. I changed the code,
and now header-auth.tsx is not a client component, yet the app works exactly the same way.
Figure 99
If you run npm run build, you will be surprised to find that the root route is now a dynamic route. Why?
Whenever you make use of this auth function behind the scenes, NextAuth is going to access and possibly modify the cookies
inside the request. We are modifying cookies or possibly reading them.
Figure 100
We've already discussed it. As we modify the cookies, the page becomes dynamic. On every page of our application, we display our header component since we included it in the layout file.
This means that every component, including the header, or every root containing the header, essentially every page in our application, is marked as dynamic.
Simply checking whether a user is logged in or not makes our page dynamic.
Figure 101
Now, we can understand why cart and payments routes are dynamic.
Figure 102
I did some testing. I commented out all the code related to authentication and ran the npm run build command. Now you can see that the /payments route is static.
Figure 103
Another dynamic route is /search.
Figure 104
In the code we see that we have used searchParams prop.
Figure 105
That made the route dynamic.
Figure 106
After commenting out the searchParams prop and building the app, we could see that the /search route converted to static.
Figure 107
We render the search box in a client component using useSearchParams. The SearchInput component is rendered inside Suspense. Let’s remove Suspense and build the app.
Figure 108
After building it, we will encounter some warnings. Client components using useSearchParams need to be wrapped with Suspense, or you will receive a strange warning at build time.