Introduction to Next.js 14
Next.js 14 is a significant version of the framework, introducing numerous new features and improvements to enhance developer productivity and application performance. Key features of Next.js 14 include:
- Automatic Optimization: Next.js 14 automatically optimizes applications without requiring manual configuration.
- Simplified File Structure: The new file structure is more streamlined, easier to understand, and maintain.
- Enhanced Performance: Improved rendering speed and loading times.
- Better Developer Experience: Offers faster hot reloading and more user-friendly error reporting.
- Improved API Support: Enhanced API route functionality, supporting more HTTP methods.
- Enhanced Type Safety: Provides better TypeScript support.
History and Features of Next.js
Next.js was first released in 2016 and has since become one of the most important frameworks in the React community. Some key features of Next.js include:
- Server-Side Rendering (SSR): Enables rendering React components on the server, improving first-page load speed and search engine optimization (SEO).
- Static Site Generation (SSG): Compiles the entire application into static HTML files, making it easy to deploy to any static file hosting service.
- Dynamic Routing: Supports dynamically generated routes, allowing pages to be added or removed at runtime.
- Hot Reloading: Automatically updates the browser content during development when files change.
- Code Splitting: Automatically splits code into smaller chunks, loading them on demand to reduce initial load time.
- Data Fetching: Provides multiple data fetching methods, such as
getServerSidePropsandgetStaticProps.
Relationship with React
Next.js is built on top of React, leveraging React’s core features like componentization and virtual DOM. Next.js provides an abstraction layer, making it easier for developers to build large-scale applications without dealing with complex configurations.
Installing Next.js and Initializing a Project
To start using Next.js 14, ensure Node.js is installed on your system. You can install Next.js and create a new project using npm or yarn.
Initializing a Project with npm
# Create a new Next.js project
npx create-next-app@latest
# Navigate to the project directory
cd my-app
# Start the development server
npm run devInitializing a Project with yarn
# Create a new Next.js project
yarn create-next-app@latest
# Navigate to the project directory
cd my-app
# Start the development server
yarn devTerminal Prompts
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes
What import alias would you like configured? @/*Basic Concepts
Routing System and Page Components
Next.js uses a file-based routing system. Each .js or .jsx file automatically generates a corresponding URL path.
- Creating the Homepage: Create a file named
index.jsin thepagesdirectory. - Creating Other Pages: Create additional
.jsfiles, such asabout.js, which will automatically map to the/aboutpath.
// pages/index.js
function Home() {
return (
<div>
<h1>Welcome to My App</h1>
<p>This is the home page.</p>
</div>
);
}
export default Home;
// pages/about.js
function About() {
return (
<div>
<h1>About Us</h1>
<p>This is the about page.</p>
</div>
);
}
export default About;Static Site Generation (SSG) and Server-Side Rendering (SSR)
Next.js supports two primary rendering modes: Static Site Generation (SSG) and Server-Side Rendering (SSR).
- Static Site Generation (SSG): Generates HTML files at build time, suitable for content that does not require frequent updates.
- Server-Side Rendering (SSR): Dynamically generates HTML files on each request, ideal for content that needs to be generated dynamically based on user requests.
SSG Example
Use the getStaticProps function to fetch data and populate the page at build time.
// pages/post/[id].js
import { useRouter } from 'next/router';
export async function getStaticProps(context) {
const { id } = context.params;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
return {
props: {
post,
},
};
}
export async function getStaticPaths() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}));
return { paths, fallback: false };
}
function Post({ post }) {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Post {id}</h1>
<p>{post.title}</p>
<p>{post.body}</p>
</div>
);
}
export default Post;SSR Example
Use the getServerSideProps function to fetch data and populate the page on each request.
// pages/post/[id].js
import { useRouter } from 'next/router';
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
return {
props: {
post,
},
};
}
function Post({ post }) {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Post {id}</h1>
<p>{post.title}</p>
<p>{post.body}</p>
</div>
);
}
export default Post;Analysis of SSG Example
- Define Page Component: The
Postcomponent receives apostprop and displays the post’s title and body. - Fetch Static Paths: The
getStaticPathsfunction defines all possible post IDs, determined at build time. - Fetch Static Props: The
getStaticPropsfunction fetches data for each post ID and passes the result aspropsto the page component.
Analysis of SSR Example
- Define Page Component: Same as the SSG example.
- Fetch Server-Side Props: The
getServerSidePropsfunction fetches data on each request and passes the result aspropsto the page component.
App Router Project Structure
App Router Overview
Next.js 14’s App Router introduces a new file structure that includes both app and pages directories. The app directory is used for components based on the App Router, while the pages directory is retained for compatibility with legacy routing.
appDirectory: Stores components using the App Router.pagesDirectory: Retained for compatibility with legacy routing.
App Router File Structure
The file structure for the App Router is as follows:
my-app/
node_modules/
app/
layout.tsx
global.css
page.tsx
(route groups)
pages/
index.tsx
public/
styles/
components/
...
package.json
next.config.jslayout.tsx: Global layout component defining the common layout for pages.global.css: Global stylesheet.page.tsx: Default page component.(route groups): Route groups for organizing pages and sharing layouts.
Creating a Global Layout
Create a file named layout.tsx in the app directory to define the global layout.
// app/layout.tsx
import './global.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/about">About</a>
</li>
</ul>
</nav>
</header>
<main>{children}</main>
<footer>
<p>© {new Date().getFullYear()} My App</p>
</footer>
</body>
</html>
);
}- Define Global Layout Component: The
RootLayoutcomponent defines the common layout for pages, including a header navigation and footer copyright. - Use Global Styles: The
global.cssfile contains global CSS styles.
Creating the Default Page
Create a file named page.tsx in the app directory to define the default page.
// app/page.tsx
export default function HomePage() {
return (
<div>
<h1>Welcome to My App</h1>
<p>This is the home page.</p>
</div>
);
}- Define Default Page Component: The
HomePagecomponent defines the content of the default page.
Creating the About Page
Create a file named about/page.tsx in the app directory to define the about page.
// app/about/page.tsx
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>This is the about page.</p>
</div>
);
}- Define About Page Component: The
AboutPagecomponent defines the content of the about page.
Creating a Dynamic Route Page
Create a file named [id]/page.tsx in the app directory to define a dynamic route page.
// app/[id]/page.tsx
import { useRouter } from 'next/router';
export default function PostPage() {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Post {id}</h1>
<p>This is the post page for ID {id}.</p>
</div>
);
}- Define Dynamic Route Page Component: The
PostPagecomponent defines the content of the dynamic route page. - Fetch Dynamic Data: Use the
getServerSidePropsfunction to fetch data on the server and pass it aspropsto the page component.
Using getServerSideProps or getStaticProps
To fetch data, use the getServerSideProps or getStaticProps functions in the page component.
// app/[id]/page.tsx
import { useRouter } from 'next/router';
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
return {
props: {
post,
},
};
}
export default function PostPage({ post }) {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Post {id}</h1>
<p>Title: {post.title}</p>
<p>Body: {post.body}</p>
</div>
);
}Using Shared Layouts
Create shared layouts for a group of pages, such as a shared layout for blog posts.
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<h2>Blog</h2>
{children}
</section>
);
}// app/blog/[id]/page.tsx
import { useRouter } from 'next/router';
export async function getServerSideProps(context) {
// ... Fetch data
}
export default function BlogPostPage({ post }) {
const router = useRouter();
const { id } = router.query;
return (
<>
<h1>Post {id}</h1>
<p>Title: {post.title}</p>
<p>Body: {post.body}</p>
</>
);
}- Define Shared Layout Component: The
BlogLayoutcomponent defines a shared layout for blog posts. - Use Shared Layout: Use the shared layout component in blog post pages.
Handling 404 Errors with not-found.tsx
Create a file named not-found.tsx in the app directory to define the 404 page.
// app/not-found.tsx
export default function NotFoundPage() {
return (
<div>
<h1>404 - Not Found</h1>
<p>The requested page could not be found.</p>
</div>
);
}- Define 404 Page Component: The
NotFoundPagecomponent defines the content of the 404 page.
Dynamic Route Parameters
Dynamic route parameters allow matching different pages based on variable parts of the URL. In Next.js 14, use square brackets ([]) to define dynamic parameters.
For example, to create dynamic routes for user profile pages:
// app/users/[username]/page.tsx
import { useRouter } from 'next/router';
export default function UserProfilePage() {
const router = useRouter();
const { username } = router.query;
return (
<div>
<h1>User Profile: {username}</h1>
<p>This is the profile page for user {username}.</p>
</div>
);
}- Define Dynamic Route Page Component: The
UserProfilePagecomponent defines the content of the dynamic route page. - Use
useRouter: Use theuseRouterhook to access dynamic parameters from the URL.
Nested Routes
Nested routes allow nesting pages and layouts within the route structure, helping organize complex page structures.
Create a nested blog page structure:
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<h2>Blog</h2>
{children}
</section>
);
}
// app/blog/page.tsx
export default function BlogIndexPage() {
return (
<div>
<h1>Blog Posts</h1>
<p>List of blog posts goes here.</p>
</div>
);
}
// app/blog/[id]/page.tsx
import { useRouter } from 'next/router';
export default function BlogPostPage() {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Blog Post {id}</h1>
<p>This is the blog post with ID {id}.</p>
</div>
);
}- Define Shared Layout Component: The
BlogLayoutcomponent defines a shared layout for blog posts. - Define Blog Index Page Component: The
BlogIndexPagecomponent defines the content of the blog index page. - Define Blog Post Page Component: The
BlogPostPagecomponent defines the content of blog post pages and uses the shared layout.
Page Lifecycle
Next.js 14 supports page lifecycle hooks, allowing specific actions to be performed before and after page loading.
Use getInitialProps and getServerSideProps to handle the page lifecycle.
// app/blog/[id]/page.tsx
import { useRouter } from 'next/router';
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
return {
props: {
post,
},
};
}
export default function BlogPostPage({ post }) {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Blog Post {id}</h1>
<p>Title: {post.title}</p>
<p>Body: {post.body}</p>
</div>
);
}- Use
getServerSideProps: Fetch data on the server and pass it aspropsto the page component.
Data Fetching
Next.js 14 provides several data fetching methods, including getStaticProps, getServerSideProps, and useEffect.
Use getStaticProps and getStaticPaths to fetch data:
// app/blog/[id]/page.tsx
import { useRouter } from 'next/router';
export async function getStaticProps(context) {
const { id } = context.params;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
return {
props: {
post,
},
};
}
export async function getStaticPaths() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}));
return { paths, fallback: false };
}
export default function BlogPostPage({ post }) {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Blog Post {id}</h1>
<p>Title: {post.title}</p>
<p>Body: {post.body}</p>
</div>
);
}- Use
getStaticPropsandgetStaticPaths: Fetch data at build time and determine all possible dynamic paths.
Error Handling
Next.js 14 provides built-in error handling mechanisms to easily catch and handle page-level errors.
Use the error.tsx file to handle page-level errors:
// app/error.tsx
export default function ErrorPage({ error, reset }: any) {
return (
<div>
<h1>Error: {error.status || 'unknown'}</h1>
<p>{error.message}</p>
<button onClick={() => reset()}>Try Again</button>
</div>
);
}- Define Error Page Component: The
ErrorPagecomponent defines the content of the error page and provides a retry button.
Page Router Project Structure
Page Router Overview
The Page Router uses a file-based routing system, where each .js or .jsx file automatically generates a corresponding URL path.
- pages Directory: Stores page components using the Page Router.
- public Directory: Stores public resource files.
Page Router File Structure
The file structure for the Page Router is as follows:
my-app/
node_modules/
pages/
index.js
about.js
users/
[username].js
blog/
index.js
[id].js
public/
styles/
components/
...
package.json
next.config.jsindex.js: Homepage component.about.js: About page component.users/[username].js: User profile page component.blog/index.js: Blog index page component.blog/[id].js: Blog post page component.
Creating the Homepage
Create a file named index.js in the pages directory to define the homepage.
// pages/index.js
function HomePage() {
return (
<div>
<h1>Welcome to My App</h1>
<p>This is the home page.</p>
</div>
);
}
export default HomePage;- Define Homepage Component: The
HomePagecomponent defines the content of the homepage.
Creating the About Page
Create a file named about.js in the pages directory to define the about page.
// pages/about.js
function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>This is the about page.</p>
</div>
);
}
export default AboutPage;- Define About Page Component: The
AboutPagecomponent defines the content of the about page.
Creating the User Profile Page
Create a file named [username].js in the pages/users directory to define the user profile page.
// pages/users/[username].js
import { useRouter } from 'next/router';
function UserProfilePage() {
const router = useRouter();
const { username } = router.query;
return (
<div>
<h1>User Profile: {username}</h1>
<p>This is the profile page for user {username}.</p>
</div>
);
}
export default UserProfilePage;- Define User Profile Page Component: The
UserProfilePagecomponent defines the content of the user profile page. - Use
useRouter: Use theuseRouterhook to access dynamic parameters from the URL.
Creating the Blog Index Page
Create a file named index.js in the pages/blog directory to define the blog index page.
// pages/blog/index.js
function BlogIndexPage() {
return (
<div>
<h1>Blog Posts</h1>
<p>List of blog posts goes here.</p>
</div>
);
}
export default BlogIndexPage;- Define Blog Index Page Component: The
BlogIndexPagecomponent defines the content of the blog index page.
Creating the Blog Post Page
Create a file named [id].js in the pages/blog directory to define the blog post page.
// pages/blog/[id].js
import { useRouter } from 'next/router';
function BlogPostPage() {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Blog Post {id}</h1>
<p>This is the blog post with ID {id}.</p>
</div>
);
}
export default BlogPostPage;- Define Blog Post Page Component: The
BlogPostPagecomponent defines the content of the blog post page. - Use
getServerSideProps: Fetch data on the server and pass it aspropsto the page component.
Using getServerSideProps or getStaticProps
To fetch data, use the getServerSideProps or getStaticProps functions in the page component.
// pages/blog/[id].js
import { useRouter } from 'next/router';
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
return {
props: {
post,
},
};
}
export default function BlogPostPage({ post }) {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Blog Post {id}</h1>
<p>Title: {post.title}</p>
<p>Body: {post.body}</p>
</div>
);
}Dynamic Route Parameters
Dynamic route parameters allow matching different pages based on variable parts of the URL. In Next.js 14, use square brackets ([]) to define dynamic parameters.
For example, to create dynamic routes for user profile pages:
// pages/users/[username].js
import { useRouter } from 'next/router';
function UserProfilePage() {
const router = useRouter();
const { username } = router.query;
return (
<div>
<h1>User Profile: {username}</h1>
<p>This is the profile page for user {username}.</p>
</div>
);
}
export default UserProfilePage;- Define Dynamic Route Page Component: The
UserProfilePagecomponent defines the content of the dynamic route page. - Use
useRouter: Use theuseRouterhook to access dynamic parameters from the URL.
Nested Routes
Nested routes allow nesting pages within the route structure, helping organize complex page structures.
Create a nested blog page structure:
// pages/blog/index.js
function BlogIndexPage() {
return (
<div>
<h1>Blog Posts</h1>
<p>List of blog posts goes here.</p>
</div>
);
}
export default BlogIndexPage;
// pages/blog/[id].js
import { useRouter } from 'next/router';
function BlogPostPage() {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Blog Post {id}</h1>
<p>This is the blog post with ID {id}.</p>
</div>
);
}
export default BlogPostPage;- Define Blog Index Page Component: The
BlogIndexPagecomponent defines the content of the blog index page. - Define Blog Post Page Component: The
BlogPostPagecomponent defines the content of the blog post page.
Page Lifecycle
Next.js 14 supports page lifecycle hooks, allowing specific actions to be performed before and after page loading.
Use getInitialProps and getServerSideProps to handle the page lifecycle.
// pages/blog/[id].js
import { useRouter } from 'next/router';
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
return {
props: {
post,
},
};
}
export default function BlogPostPage({ post }) {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Blog Post {id}</h1>
<p>Title: {post.title}</p>
<p>Body: {post.body}</p>
</div>
);
}- Use
getServerSideProps: Fetch data on the server and pass it aspropsto the page component.
Data Fetching
Next.js 14 provides several data fetching methods, including getStaticProps, getServerSideProps, and useEffect.
Use getStaticProps and getStaticPaths to fetch data:
// pages/blog/[id].js
import { useRouter } from 'next/router';
export async function getStaticProps(context) {
const { id } = context.params;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
return {
props: {
post,
},
};
}
export async function getStaticPaths() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}));
return { paths, fallback: false };
}
export default function BlogPostPage({ post }) {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Blog Post {id}</h1>
<p>Title: {post.title}</p>
<p>Body: {post.body}</p>
</div>
);
}- Use
getStaticPropsandgetStaticPaths: Fetch data at build time and determine all possible dynamic paths.
Error Handling
Next.js 14 provides built-in error handling mechanisms to easily catch and handle page-level errors.
Use the _error.js file to handle page-level errors:
// pages/_error.js
export default function ErrorPage({ statusCode }) {
return (
<div>
<h1>Error: {statusCode || 'unknown'}</h1>
<p>An error occurred while loading this page.</p>
</div>
);
}- Define Error Page Component: The
ErrorPagecomponent defines the content of the error page.



