Start building modern web applications using React and Next.js.
Node 18+
Git 2+
pnpm 8
- Get the template using
npx degit
:
npx degit teo-garcia/react-template-next my_project
- Install the dependencies:
pnpm install
- Run the project:
pnpm dev
- Next 14.
- React 18.
- Tailwind 3.
- Typescript 4.
- ESLint 8 + Prettier 3.
- Husky 8 + Lint Staged 15.
- Jest 29 + Testing Library React 14.
- Playwright 1.
- MSW 2.
Command | Description |
---|---|
dev | Run dev:web . |
build | Run build:web . |
dev:web | Run next in DEV mode. |
build:web | Run next in PROD mode. |
start:web | Run next server (build required). |
test:unit | Run jest . |
test:e2e | Run dev , and playwright . |
lint:css | Lint CSS files. |
lint:js | Lint JS files. |
create:component | Creates a component at src/components/<name> . |
create:feature | Creates a component at src/features/<name> . |
An overview of the folder and file conventions in Next.js and tips for organizing your project.
- app: App Router
- pages: Pages Router
- public: Static assets to be served
- src: Optional application source folder
- next.config.js: Configuration file for Next.js
- package.json: Project dependencies and scripts
- instrumentation.ts: OpenTelemetry and Instrumentation file
- middleware.ts: Next.js request middleware
- .env: Environment variables
- .env.local: Local environment variables
- .env.production: Production environment variables
- .env.development: Development environment variables
- .eslintrc.json: Configuration file for ESLint
- .gitignore: Git files and folders to ignore
- next-env.d.ts: TypeScript declaration file for Next.js
- tsconfig.json: Configuration file for TypeScript
- jsconfig.json: Configuration file for JavaScript
- layout.js/.jsx/.tsx: Layout
- page.js/.jsx/.tsx: Page
- loading.js/.jsx/.tsx: Loading UI
- not-found.js/.jsx/.tsx: Not found UI
- error.js/.jsx/.tsx: Error UI
- global-error.js/.jsx/.tsx: Global error UI
- route.js/.ts: API endpoint
- template.js/.jsx/.tsx: Re-rendered layout
- default.js/.jsx/.tsx: Parallel route fallback page
- folder/: Route segment
- folder/folder/: Nested route segment
- [folder]: Dynamic route segment
- [...folder]: Catch-all route segment
- [[...folder]]: Optional catch-all route segment
- (folder): Group routes without affecting routing
- _folder: Opt folder and all child segments out of routing
- @folder: Named slot
- (.)folder: Intercept same level
- (..)folder: Intercept one level above
- (...)folder: Intercept from root
- favicon.ico: Favicon file
- icon.(ico|jpg|jpeg|png|svg): App Icon file
- icon.(js|ts|tsx): Generated App Icon
- apple-icon.(jpg|jpeg|png): Apple App Icon file
- apple-icon.(js|ts|tsx): Generated Apple App Icon
- opengraph-image.(jpg|jpeg|png|gif): Open Graph image file
- opengraph-image.(js|ts|tsx): Generated Open Graph image
- twitter-image.(jpg|jpeg|png|gif): Twitter image file
- twitter-image.(js|ts|tsx): Generated Twitter image
- sitemap.xml: Sitemap file
- sitemap.(js|ts): Generated Sitemap
- robots.txt: Robots file
- robots.(js|ts): Generated Robots file
The React components defined in special files of a route segment are rendered in this hierarchy:
- layout.js
- template.js
- error.js (React error boundary)
- loading.js (React suspense boundary)
- not-found.js (React error boundary)
- page.js or nested layout.js
In nested routes, components of a segment nest inside the parent segment's components.
- Route structure is defined by folders.
- Public access is controlled by the presence of page.js or route.js in a folder.
- Prefix folders with _ to make them private and opt out of routing.
- Example: _folderName
- Wrap a folder in parentheses to group routes without affecting the URL path.
- Example: (folderName)
- Store application code (including app) inside an optional src directory.
- Store project files outside of app: Application code in shared folders at the root.
- Store project files in app: Shared folders within the app directory.
- Split project files by feature or route: Globally shared code in app, specific code colocated within route segments.
Store static files (e.g., images, fonts) in the public
folder. These files are accessible via the base URL (/
).
import Image from 'next/image'
export default function Page() {
return <Image src="" alt="" />
}
import Image from 'next/image'
import profilePic from './me.png'
export default function Page() {
return (
<Image
src={profilePic}
alt="Picture of the author"
// width={500} automatically provided
// height={500} automatically provided
// blurDataURL="data:..." automatically provided
// placeholder="blur" // Optional blur-up while loading
/>
)
}
import Image from 'next/image'
export default function Page() {
return (
<Image
src="https://s3.amazonaws.com/my-bucket/profile.png"
alt="Picture of the author"
width={500}
height={500}
/>
)
}
import { NextConfig } from 'next'
const config: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 's3.amazonaws.com',
pathname: '/my-bucket/**',
},
],
},
}
export default config
import { Geist } from 'next/font/google'
const geist = Geist({
subsets: ['latin'],
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={geist.className}>
<body>{children}</body>
</html>
)
}
import { Roboto } from 'next/font/google'
const roboto = Roboto({
weight: '400',
subsets: ['latin'],
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={roboto.className}>
<body>{children}</body>
</html>
)
}
import localFont from 'next/font/local'
const myFont = localFont({
src: './my-font.woff2',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={myFont.className}>
<body>{children}</body>
</html>
)
}
const roboto = localFont({
src: [
{ path: './Roboto-Regular.woff2', weight: '400', style: 'normal' },
{ path: './Roboto-Italic.woff2', weight: '400', style: 'italic' },
{ path: './Roboto-Bold.woff2', weight: '700', style: 'normal' },
{ path: './Roboto-BoldItalic.woff2', weight: '700', style: 'italic' },
],
})
Fetching data and streaming are essential to delivering dynamic content and optimizing user experience in modern web applications. This section explains how to efficiently fetch data on the server or client and how to leverage streaming for faster page loads.
Server Components allow you to fetch data directly on the server, optimizing performance by keeping large data handling and API calls off the client side.
The fetch
API is a versatile method to retrieve data from external APIs or services. It is ideal for server-side data fetching, leveraging asynchronous calls to streamline rendering.
// app/blog/page.tsx
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Using an ORM or database integration lets you fetch data directly from a backend system. This method is useful for dynamic or personalized content that depends on structured data.
// app/blog/page.tsx
import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Client Components handle data fetching on the client side, enabling features like user interaction and state management. These are suitable for dynamic updates and real-time data.
The use
hook allows for streaming data from the server to the client, improving rendering efficiency. It integrates seamlessly with Suspense
to manage loading states effectively.
// app/blog/page.tsx
import Posts from '@/app/ui/posts'
import { Suspense } from 'react'
export default function Page() {
const posts = getPosts()
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
)
}
// app/ui/posts.tsx
;('use client')
import { use } from 'react'
export default function Posts({
posts,
}: {
posts: Promise<{ id: string; title: string }[]>
}) {
const data = use(posts)
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Community libraries like SWR and React Query simplify data fetching by providing features like caching, retries, and real-time updates, making them excellent for client-side data management.
'use client'
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then((r) => r.json())
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
'https://api.vercel.app/blog',
fetcher
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
The loading.js
file is used to create a global loading state for a route. It displays fallback content while data is being fetched, ensuring a smooth user experience during navigation.
// app/blog/loading.tsx
export default function Loading() {
return <div>Loading...</div>
}
<Suspense>
enables fine-grained control over loading states, allowing developers to progressively stream content while displaying fallback components for incomplete data.
// app/blog/page.tsx
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
Creating meaningful loading states ensures users receive immediate feedback during navigation. Use skeleton screens, spinners, or placeholders to enhance user experience while data loads.
Design loading states that provide meaningful feedback to users, such as skeleton screens or spinners. Use React Devtools to preview loading states during development.
Mutating data in Next.js is essential for creating dynamic applications that require updates to server-side data or user interactions. This section covers how to create, invoke, and manage Server Functions for mutating data efficiently.
Server Functions are defined using the use server
directive, enabling safe server-side data mutations. Use a separate file to manage these functions for better organization.
// app/lib/actions.ts
'use server'
export async function createPost(formData: FormData) {
// Logic to create a new post
}
export async function deletePost(formData: FormData) {
// Logic to delete a post
}
Server Functions can be inlined within Server Components using the use server
directive.
// app/page.tsx
export default async function Page() {
async function createPost() {
'use server'
// Mutate data
}
return <></>
}
Server Functions cannot be defined in Client Components but can be invoked by importing them.
// app/ui/button.tsx
'use client'
import { createPost } from '@/app/lib/actions'
export function Button() {
return <button formAction={createPost}>Create</button>
}
Server Functions can be invoked directly from forms using the action
attribute. The function automatically receives the FormData
object.
// app/ui/form.tsx
import { createPost } from '@/app/lib/actions'
export function Form() {
return (
<form action={createPost}>
<input type="text" name="title" />
<input type="text" name="content" />
<button type="submit">Create</button>
</form>
)
}
Server Functions can also be invoked using event handlers in Client Components.
// app/ui/like-button.tsx
'use client'
import { incrementLike } from '@/app/lib/actions'
import { useState } from 'react'
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)
return (
<button
onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}
>
Like
</button>
)
}
Use the useActionState
hook to show a loading indicator while a Server Function is executing.
// app/ui/button.tsx
'use client'
import { useActionState } from 'react'
import { createPost } from '@/app/lib/actions'
export function Button() {
const [state, action, pending] = useActionState(createPost, false)
return (
<button onClick={async () => action()}>
{pending ? 'Loading...' : 'Create Post'}
</button>
)
}
Revalidate the Next.js cache after a mutation to show updated data.
// app/lib/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
// Mutate data
revalidatePath('/posts')
}
Redirect users after a mutation using the redirect
function.
// app/lib/actions.ts
'use server'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
// Mutate data
redirect('/posts')
}
Error handling in Next.js is essential for building robust applications that gracefully manage both expected errors and uncaught exceptions. This section covers handling errors in Server and Client Components, as well as creating custom error boundaries and global error handling.
Expected errors occur during normal application operation, such as validation errors or failed requests. These should be explicitly handled and communicated to the client.
Use the useActionState
hook to manage Server Function states and handle expected errors without using try/catch
.
// app/actions.ts
'use server'
export async function createPost(prevState: any, formData: FormData) {
const title = formData.get('title')
const content = formData.get('content')
const res = await fetch('https://api.vercel.app/posts', {
method: 'POST',
body: { title, content },
})
const json = await res.json()
if (!res.ok) {
return { message: 'Failed to create post' }
}
}
// app/ui/form.tsx
;('use client')
import { useActionState } from 'react'
import { createPost } from '@/app/actions'
const initialState = { message: '' }
export function Form() {
const [state, formAction, pending] = useActionState(createPost, initialState)
return (
<form action={formAction}>
<label htmlFor="title">Title</label>
<input type="text" id="title" name="title" required />
<label htmlFor="content">Content</label>
<textarea id="content" name="content" required />
{state?.message && <p aria-live="polite">{state.message}</p>}
<button disabled={pending}>Create Post</button>
</form>
)
}
In Server Components, conditionally render error messages or redirect based on the response.
// app/page.tsx
export default async function Page() {
const res = await fetch('https://...')
const data = await res.json()
if (!res.ok) {
return 'There was an error.'
}
return '...'
}
Use the notFound
function for 404 errors and define a not-found.js
file for custom 404 UI.
// app/blog/[slug]/page.tsx
import { getPostBySlug } from '@/lib/posts'
export default function Page({ params }: { params: { slug: string } }) {
const post = getPostBySlug(params.slug)
if (!post) {
notFound()
}
return <div>{post.title}</div>
}
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return <div>404 - Page Not Found</div>
}
Uncaught exceptions indicate bugs or issues outside the normal flow. These should be managed with error boundaries.
Define error.js
files to create error boundaries for route segments.
// app/dashboard/error.tsx
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
Use global-error.js
for application-wide error boundaries. This replaces the root layout in case of an error.
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
Error handling in Next.js is essential for building robust applications that gracefully manage both expected errors and uncaught exceptions. This section covers handling errors in Server and Client Components, as well as creating custom error boundaries and global error handling.
Expected errors occur during normal application operation, such as validation errors or failed requests. These should be explicitly handled and communicated to the client.
Use the useActionState
hook to manage Server Function states and handle expected errors without using try/catch
.
// app/actions.ts
'use server'
export async function createPost(prevState: any, formData: FormData) {
const title = formData.get('title')
const content = formData.get('content')
const res = await fetch('https://api.vercel.app/posts', {
method: 'POST',
body: { title, content },
})
const json = await res.json()
if (!res.ok) {
return { message: 'Failed to create post' }
}
}
// app/ui/form.tsx
;('use client')
import { useActionState } from 'react'
import { createPost } from '@/app/actions'
const initialState = { message: '' }
export function Form() {
const [state, formAction, pending] = useActionState(createPost, initialState)
return (
<form action={formAction}>
<label htmlFor="title">Title</label>
<input type="text" id="title" name="title" required />
<label htmlFor="content">Content</label>
<textarea id="content" name="content" required />
{state?.message && <p aria-live="polite">{state.message}</p>}
<button disabled={pending}>Create Post</button>
</form>
)
}
In Server Components, conditionally render error messages or redirect based on the response.
// app/page.tsx
export default async function Page() {
const res = await fetch('https://...')
const data = await res.json()
if (!res.ok) {
return 'There was an error.'
}
return '...'
}
Use the notFound
function for 404 errors and define a not-found.js
file for custom 404 UI.
// app/blog/[slug]/page.tsx
import { getPostBySlug } from '@/lib/posts'
export default function Page({ params }: { params: { slug: string } }) {
const post = getPostBySlug(params.slug)
if (!post) {
notFound()
}
return <div>{post.title}</div>
}
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return <div>404 - Page Not Found</div>
}
Uncaught exceptions indicate bugs or issues outside the normal flow. These should be managed with error boundaries.
Define error.js
files to create error boundaries for route segments.
// app/dashboard/error.tsx
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
Use global-error.js
for application-wide error boundaries. This replaces the root layout in case of an error.
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
Next.js uses a file-system based routing system, allowing you to define routes using folders and files. This section explains how to create layouts, pages, and nested routes, as well as how to navigate between them.
A page is a UI rendered on a specific route. To create a page, add a page
file inside the app
directory and export a React component.
// app/page.tsx
export default function Page() {
return <h1>Hello Next.js!</h1>
}
A layout is a shared UI that persists across multiple pages. Layouts preserve state, remain interactive, and do not rerender during navigation. Define a layout by exporting a React component with a children
prop.
// app/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<main>{children}</main>
</body>
</html>
)
}
The above layout, defined in the root app
directory, is a root layout. Root layouts must include <html>
and <body>
tags.
Nested routes are composed of multiple URL segments. For example, the /blog/[slug]
route consists of:
/
(Root Segment)blog
(Segment)[slug]
(Dynamic Segment)
To create nested routes, use folders to define segments and page
files to define the UI for each segment.
// app/blog/page.tsx
import { getPosts } from '@/lib/posts'
import { Post } from '@/ui/post'
export default async function Page() {
const posts = await getPosts()
return (
<ul>
{posts.map((post) => (
<Post key={post.id} post={post} />
))}
</ul>
)
}
// app/blog/[slug]/page.tsx
export default function Page() {
return <h1>Hello, Blog Post Page!</h1>
}
Dynamic routes, defined using square brackets (e.g., [slug]
), generate multiple pages from data.
Layouts can be nested by adding layout
files to specific route segments. Nested layouts wrap their child layouts via the children
prop.
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}
For example, the root layout (app/layout.tsx
) wraps the blog layout (app/blog/layout.tsx
), which in turn wraps the blog page (app/blog/page.tsx
) and blog post page (app/blog/[slug]/page.tsx
).
Use the <Link>
component for client-side navigation. This component extends the HTML <a>
tag with prefetching capabilities for improved performance.
// app/ui/post.tsx
import Link from 'next/link'
export default function Post({
post,
}: {
post: { slug: string; title: string }
}) {
return (
<li>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
)
}
Alternatively, use the useRouter
hook for programmatic navigation and more advanced use cases.
Next.js offers several ways to navigate between routes, providing flexibility and performance optimizations. This section explains the main methods for linking and navigating, including <Link>
, useRouter
, redirect
, and the native History API.
The <Link>
component extends the HTML <a>
tag for client-side navigation with prefetching capabilities. It is the recommended method for linking between routes.
// app/page.tsx
import Link from 'next/link'
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>
}
The useRouter
hook enables programmatic navigation in Client Components.
// app/page.tsx
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}
The redirect
function handles navigation in Server Components. It can be used for conditional redirects during server-side rendering.
// app/team/[id]/page.tsx
import { redirect } from 'next/navigation'
async function fetchTeam(id: string) {
const res = await fetch('https://...')
if (!res.ok) return undefined
return res.json()
}
export default async function Profile({
params,
}: {
params: Promise<{ id: string }>
}) {
const id = (await params).id
if (!id) {
redirect('/login')
}
const team = await fetchTeam(id)
if (!team) {
redirect('/join')
}
}
Next.js integrates with the browser's History API, allowing you to use pushState
and replaceState
for navigation without full-page reloads.
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder: string) {
const params = new URLSearchParams(searchParams.toString())
params.set('sort', sortOrder)
window.history.pushState(null, '', `?${params.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>Sort Ascending</button>
<button onClick={() => updateSorting('desc')}>Sort Descending</button>
</>
)
}
'use client'
import { usePathname } from 'next/navigation'
export function LocaleSwitcher() {
const pathname = usePathname()
function switchLocale(locale: string) {
const newPath = `/${locale}${pathname}`
window.history.replaceState(null, '', newPath)
}
return (
<>
<button onClick={() => switchLocale('en')}>English</button>
<button onClick={() => switchLocale('fr')}>French</button>
</>
)
}
Next.js splits application code by route segments, reducing the data needed for navigation.
Routes are prefetched using the <Link>
component or router.prefetch
.
The Router Cache stores React Server Component payloads for faster navigation.
Only the route segments that change re-render on the client, preserving shared layouts and improving performance.
Soft navigation ensures only modified route segments re-render, maintaining React state across navigations.
Scroll position is preserved, and cached segments are reused during backward and forward navigation.
Next.js manages routing transitions between pages/
and app/
directories during incremental migration.