useEffect - Side Effects, Data Fetching & Async State
Browser DevTools - The Network Tab
So far you've been writing React components in isolation: a single app with a single view. Most real products have multiple pages: a home page, an about page, a user profile, a settings screen. Managing that in plain React requires setting up a router yourself (with React Router, for example). Next.js gives you one out of the box, and it works differently from anything you've seen before: the file system is your router.
This chapter covers how to set up a Next.js project, what the generated files actually do, and how the App Router turns folders and files into navigable pages.
<aside> 💡
Info
Next.js isn't a niche tool — it's the dominant way React applications are built professionally. According to the 2025 State of JavaScript survey, Next.js is used by 59% of JavaScript developers surveyed InfoQ, making it the most used meta-framework in the ecosystem ZenRows. In 2024, React held 69.9% usage and Next.js 52.9% among frontend developers The Software House (meaning more than half of all React developers are already building with Next.js)
You'll encounter it in job listings, in open-source projects, and in codebases at companies of every size. Learning it now isn't getting ahead of yourself — it's meeting the industry where it already is.
</aside>
Next.js is a framework built on top of React. Where React is a library for building UI components, Next.js adds the full application layer around those components: routing, server-side rendering, static generation, API routes, optimised images, and more.
You already know React — Next.js is how you take that knowledge and build something production-ready. It handles the decisions you'd otherwise have to make yourself (or get wrong).
https://www.youtube.com/watch?v=xnOwOBYaA3w
<aside> 💡
Info
Next.js has two routing systems: the older Pages Router (using a pages/ directory) and the newer App Router (using an app/ directory). The App Router has been the default since Next.js 13 and is where the framework is headed. All new projects should use it, and this is what you'll learn here. If you open an older codebase and see a pages/ directory, that's the Pages Router — the concepts are similar but the file conventions differ.
</aside>
Run this command in your terminal:
npx create-next-app@latest my-app
You'll be asked a few questions. Choose these settings:
Would you like to use TypeScript? › Yes
Would you like to use ESLint? › Yes
Would you like to use Tailwind CSS? › No (we'll add it in the next chapter)
Would you like your code inside a `src/` directory? › Yes
Would you like to use App Router? › Yes
Would you like to use Turbopack for next dev? › Yes
Would you like to customize the import alias? › No
Once it finishes, open the project and start the development server:
cd my-app
npm run dev
Open http://localhost:3000 — you'll see the default Next.js welcome page. You're running.
<aside> 💡
⌨️ Hands On
Run the setup above, start the dev server, and open http://localhost:3000. Then open the project in your editor and spend two minutes just looking at what was generated before reading the next section. What folders do you see? What files look familiar from your React experience?
</aside>
Here's what create-next-app generates (with the src/ option selected):
my-app/
├── src/
│ └── app/
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── public/
│ └── (static assets: images, fonts, etc.)
├── next.config.ts
├── tsconfig.json
├── package.json
└── .eslintrc.json
That's it for what you need to care about right now. The rest of the files are configuration that create-next-app handles for you.
src/app/page.tsx — This is your homepage. Whatever this file exports is what renders at /. This is where you'll spend most of your time as you add content to your site.
src/app/layout.tsx — This is the root layout. It wraps every page in your application. Things like your <html> and <body> tags live here, along with anything that should appear on every page — a navigation bar, a footer, a global font. It accepts a children prop which is replaced by the current page.
src/app/globals.css — Global CSS that applies across the whole app. Imported in layout.tsx.
public/ — Static files served directly. An image at public/avatar.png is accessible at /avatar.png in the browser. No imports needed.
next.config.ts — Next.js configuration. You'll rarely need to touch this at your current stage.
tsconfig.json — TypeScript configuration. create-next-app sets this up correctly. Leave it alone unless you know what you're changing.
The .next/ directory (created when you first run npm run dev) is Next.js's build output. Never edit it manually — it gets regenerated on every build.
node_modules/ is your installed dependencies. Same rule: never edit, always ignore in version control.
<aside> 💡
Info
Notice the @/ import alias in the generated code. @/ maps to your src/ directory. So import something from '@/components/Button' is equivalent to import something from '../../components/Button' but much cleaner. This is configured in tsconfig.json and works out of the box.
</aside>
In the App Router, folders inside app/ define URL segments. The rule is simple:
about maps to the route /aboutprojects maps to /projectspage.tsx file inside that folder is what actually renders at that URLWithout a page.tsx, the folder creates no route — it exists in the file system but produces no page. The file is the thing that makes a route public.
src/app/
├── page.tsx → renders at /
├── about/
│ └── page.tsx → renders at /about
└── projects/
└── page.tsx → renders at /projects
To add a new page to your app, you create a folder and add a page.tsx inside it. That's the entire workflow.
A page is just a React component, exported as the default export:
// src/app/about/page.tsx
export default function AboutPage() {
return (
<main>
<h1>About</h1>
<p>This is the about page.</p>
</main>
);
}
Create this file, and /about immediately becomes a working route. No configuration, no route registration — just the file.
<aside> ⌨️
Hands On
Create a new folder at src/app/about/ and add a page.tsx file that exports a simple component with a heading and a paragraph. Navigate to http://localhost:3000/about in your browser. The page should appear immediately without restarting the dev server.
</aside>
Routes nest the same way folders nest. To create /projects/web:
src/app/
└── projects/
├── page.tsx → renders at /projects
└── web/
└── page.tsx → renders at /projects/web
Each folder adds a segment to the URL. You can go as deep as you need.
Many pages display content based on a variable — a user ID, a post slug, a product handle. You don't create a separate folder for each one; you use a dynamic segment by wrapping a folder name in square brackets.
src/app/
└── users/
└── [id]/
└── page.tsx → renders at /users/1, /users/42, /users/anything
Inside the page component, the dynamic value is available as a prop:
// src/app/users/[id]/page.tsx
interface Props {
params: Promise<{ id: string }>;
}
export default async function UserPage({ params }: Props) {
const { id } = await params;
return (
<main>
<h1>User {id}</h1>
</main>
);
}
Navigate to /users/42 and id will be "42". Navigate to /users/sarah and id will be "sarah".
<aside> 💡
Info
Note that params is a Promise in the App Router — you need to await it. This is a Next.js convention that enables certain performance optimisations on the server. The async keyword on the component function is what makes this possible, and it's perfectly valid in Next.js — server components can be async functions.
</aside>
Open src/app/layout.tsx. It looks something like this:
// src/app/layout.tsx
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'My App',
description: 'Built with Next.js',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
</body>
</html>
);
}
Every page in your app renders inside this layout. The {children} is replaced by whatever the current page exports.
This is where you'd put a navigation bar that should appear on every page:
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>{/* navigation links go here */}</nav>
</header>
<main>{children}</main>
<footer>
<p>© 2025 My App</p>
</footer>
</body>
</html>
);
}
The metadata export at the top is how Next.js handles <title> and <meta> tags. You can export metadata from any page.tsx or layout.tsx to set per-page metadata. Next.js injects it into the <head> automatically.
// src/app/about/page.tsx
export const metadata = {
title: 'About — My App',
description: 'Learn more about us.',
};
export default function AboutPage() {
return <h1>About</h1>;
}
You can add a layout.tsx to any route folder, not just the root. A nested layout wraps only the pages within that route segment and its children:
src/app/
├── layout.tsx ← wraps everything
└── dashboard/
├── layout.tsx ← wraps only /dashboard and its children
└── page.tsx
This is useful for adding a sidebar or secondary navigation that only appears in a specific section of the app, without affecting other pages.
In standard HTML, you navigate between pages with <a href="/about">. In Next.js, you use the Link component instead:
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/projects">Projects</Link>
</nav>
);
}
Link renders as an <a> tag in the HTML, so it's fully accessible — keyboard navigable, works with screen readers, right-click to open in new tab. But under the hood, Next.js intercepts the click and handles the navigation without a full page reload. The user experience is instant, like a single-page app, while the URL and browser history still work correctly.
<a>?A plain <a href="/about"> works, but it triggers a full page reload: the browser discards all current JavaScript state, re-fetches the HTML, re-downloads the JavaScript, and starts over. Link does client-side navigation: only the changed content updates, and any state that lives in the layout (like an open menu or animation) is preserved.