Next.js tutorial
Chapter 1
Clone the starter repository
- written in ts
- refrects modern web development landscape
npm i
npm run dev
Chapter 2
global.css
: responsible for global styling. it is intended to be imported in layout.tsx
file.
if global.css contains Tailwind configuration. Styles can be applied through classname from anywhere withtout namespace collision.
Using the clsx library to toggle class names
styles can be toggled based on property using clsx
<span
className={clsx(
'inline-flex items-center rounded-full px-2 py-1 text-xs',
{
'bg-gray-100 text-gray-500': status === 'pending',
'bg-green-500 text-white': status === 'paid',
},
)}
>
Chapter 3
Font
Use of next/font/google
prevents from Cumulative Layout Shift when a page fully loaded.
/app/ui/fonts.ts
import { Inter, Lusitana } from 'next/font/google'
export const inter = Inter({ subsets: ['latin']})
export const lusitana = Lusitana({ weight: ["400", "700"], subsets: ['latin']})
Image
next/image
optimizes related to image like generating multiple size, use modern compressing mothod on my behalf. Just replace img
component with Image
component.
Chapter 4: Creating Layouts and Pages
Router
page router are based on the directories and can be routed when exported Page
function exists under the app
dir.
Layout
partial rendering, that renders only a child component, and shared styling can be done by exporting Layout
func.
Chapter 5: Navigating Between Pages
Next provide Link
component alternates <a>
tag. Link
not only routes users to destination, but also prefetches when appears on the page backgroud and navigate there instantly when pressed.
It's a React component enables client side transitions.
Also, usePathname
from next/navigation
returns current page's path that is used to to toggle button designs using clsx for example.
Chapter 6: Setting Up Your Database
This chapter provides the instruction to populate postgresdb on vercel and seed with initial data.
Chapter 7: Fetching Data
Using React server component to interact with DB that makes securely without requiring additional API layer because it won't expose secrets to clients.
Chapter 8: Static and Dynamic Rendering
Static rendering is preferred over Dynamic rendering when fulfill the following situation
- Data is less updated and no frequent udpate required
- Data is common for all users
In contrast, the situation like user dashboard, the latter rendering option is preferred.
In Next.js, function results are cached by default and can be invalidate using unstable_noStore
.
import { unstable_noStore as noStore } from 'next/cache';
Chapter 9: Streaming
There is a case data fetch blocks a hole page rendered on dynamic rendering.
Use streaming to render the part of the page to be rendered when data is ready.
There are two way to do, loading.tsx
and <Suspense>
.
loading.tsx
Show skelton components which embed as static component until all dynamic rendering done in the page.tsx
import DashboardSkeleton from "../../ui/skeletons"
export default function Loading() {
return <DashboardSkeleton />
}
Suspense
To give more granular control on streaming by component level. Use Suspense
which is a component of react framework. It is preferred over using loading.tsx
.
import { Suspense } from "react";
import { SlowComponent } from "../ui/someComponents";
export default async function Page() {
return (
<main>
<h1>Page</h1>
<div>
<Suspense fallback={<h1>loading</h1>}>
<SlowComponent />
</Suspense>
</div>
</main>
);
}
Chapter 10: Partial Prerendering (Optional)
Almost all pages have partial part and dynamic part. Next.js 14 introduced the experimental feature called Partial Prerendering which enables this idea by using Suspense as placeholder until dynamic data being loaded.
Chapter 11: Adding Search and Pagination
Search
Navigate user with queried page when they put it in search box, onChange
event in the input tag will invoke this. Use Debounce
is a best practice to reduce DB load unless number of requests will grow extremetely.
The list of benefits using URL search params have several benefits can be listed below.
- Bookmarkable and Shareable URLs
- Server-Side Rendering and Initial Load
- Analytics and Tracking
Use useRouter's replace
function to navigate to the page. The URL parameter is updated so user can share and bookmark it.
const handleSearch = useDebouncedCallback((term: string) => {
const params = new URLSearchParams(searchParams);
params.set("page", "1");
if (term) {
params.set("query", term);
} else {
params.delete("query");
}
replace(`${pathname}?${params.toString()}`);
}, 500);
Pagination
Adding pagination allows user to fetch limited number of items queued to backend services to control demand. Embedding href
in page buttons are shown in this tutorial.
const createPageURL = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
params.set("page", pageNumber.toString());
return `${pathname}?${params.toString()}`;
};
<div className="flex -space-x-px">
{allPages.map((page, index) => {
let position: "first" | "last" | "single" | "middle" | undefined;
if (index === 0) position = "first";
if (index === allPages.length - 1) position = "last";
if (allPages.length === 1) position = "single";
if (page === "...") position = "middle";
return (
<PaginationNumber
key={page}
href={createPageURL(page)}
page={page}
position={position}
isActive={currentPage === page}
/>
);
})}
</div>
Chapter 12: Mutating Data
- React's Server Action
-
FormData
vlidation and parsing - Revalidate and Redirect
- Dynamic routing
Server Action
Mutate data when form is submitted without creating API endpoint. React creates POST
API behind the scence with many security capability.
// Server Component
export default function Page() {
// Action
async function create(formData: FormData) {
'use server';
// Logic to mutate data...
}
// Invoke the action using the "action" attribute
return <form action={create}>...</form>;
}
"use server";
directive informs React that is a Server Action.
Data validation and parsing
Create a z.object
const FormSchema = z.object({
id: z.string(),
customerId: z.string(),
amount: z.coerce.number(),
status: z.enum(["pending", "paid"]),
date: z.string(),
});
const CreateInvoice = FormSchema.omit({ id: true, date: true });
Parse the data from FormData
const { customerId, amount, status } = CreateInvoice.parse({
customerId: formData.get("customerId"),
amount: formData.get("amount"),
status: formData.get("status"),
});
Revalidate and Redirect
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createInvoice(formData: FormData) {
// do something
revalidatePath("/dashborad/invoices");
redirect("/dashboard/invoices");
}
### Dynamic routing
https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes
Accomplished by creating a directory surrounded with bracket `[]`; `[id]`, `[post]`, `[slug]`.
### Passing argument to Server Action
```tsx
// Passing an id as argument won't work
<form action={updateInvoice(id)}>
const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
<form action={updateInvoiceWithId} />
Further reading
Chapter 13: Handling Error
- Handle unexptected error, and show fallback UI to the user.
- Use
notFound
function andnot-found
file to handle 404 errors.
Use try-catch syntax and error page
Throw error when unexpected error occurs.
The error thrown by components are handled with the framework, configured error page rendred if error.tsx
present in the route as handler.
try {
doSomething();
} catch(error) {
throw new Error("Failed to doSomething!");
}
The type of Error is a JavaScript object that passed into the error component.
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string};
reset: () => void;
}) {
useEffect(() => {
console.log(error);
}, [error]);
return (
// render something and call reset();
);
}
Use reset
function to delegate framework to render root element.
notFound function and not-found page
import { notFound } from 'next/navigation';
notFound();
Use notFound()
function and place not-found.tsx
to show it to the user.
Further Reading
Chapter 14: Improving Accessibility
Improve Accesibility
eslint-plugin-jsx-a11y is a linter validate site a11y by configuring package.json
and run npm run lint
.
{
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start",
"seed": "node -r dotenv/config ./scripts/seed.js",
+ "lint": "next lint"
}
}
Implement server-side form validation
Validate input data through form on server side component.
- Pass a state variable defined in local component and serverside function into
useFormState
- Pass handler function from
useFormState
function to action attribute in a form - Dispatched logic runs on server-side and should return error through defined State type in the lib
- Use
aria-describedby
for a11y and switch visibility using ternary operator
Form data validation done by zod.
Chapter 15: Adding Authentication
Authentication
: Verify the user who they are. (Differ from Authorization
)
- Create Login route
- Add middleware to route login page safely
- Add server component to execute
authenticate
function from form which declared withuse client
directive
export default function LoginForm() {
const [errorMessage, dispatch] = useFormState(authenticate, undefined);
return (
<form action={dispatch}>
// form input
</form>
}
Password hashing is done by using bcrypt
in this example. However, external credentials provider like Github, Gmail are preferred over than implementing user/password
model manually.
Chapter 16: Adding Metadata
Add metadata to help improve SEO. Graphical images shown up as an embedding in SNS posts, fields gives great chance to attract users by instrumenting open-graph image.
import { Metadata } from "next";
export const metadata: Metadata = {
// configurations for Metadata through next API
}