Working with Pages & Layouts in Next.js 14
In the ever-evolving world of web development, building modern and performant applications has become a top priority. Next.js, a React framework created by Vercel, has gained immense popularity for its ability to simplify the process of creating server-rendered React applications. With the release of Next.js 14, developers have been introduced to exciting new features and improvements, among which are the enhanced capabilities for working with pages and layouts.
Pages in Next.js 14
At the core of Next.js lies its file-based routing system, where each file in the pages
directory corresponds to a route in your application. This intuitive approach makes it easy to create and manage pages, as well as to reason about the structure of your application.
Creating a New Page
To create a new page in Next.js 14, simply add a new file (e.g., about.js
) to the pages
directory. By convention, the file name corresponds to the route path (/about
in this case). Here's an example of a basic page component:
// pages/about.js
import React from 'react';
const AboutPage = () => {
return (
<div>
<h1>About Us</h1>
<p>Welcome to our awesome website!</p>
</div>
);
};
export default AboutPage;
Dynamic Pages and Routing
Next.js 14 also supports dynamic pages and routing, which allows you to create pages that can handle dynamic data. For example, you might want to create a page that displays information about a specific product based on its ID. To achieve this, you can use dynamic imports and dynamic routing.
// pages/products/[id].js
import React from 'react';
import { useRouter } from 'next/router';
const ProductPage = () => {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>Product: {id}</h1>
{/* Fetch and display product details */}
</div>
);
};
export default ProductPage;
In this example, the [id].js
file in the pages/products
directory represents a dynamic page. The useRouter
hook from Next.js provides access to the current route parameters, allowing you to fetch and display data specific to the requested product ID.
Layouts in Next.js 14
Layouts in Next.js 14 provide a way to share common UI elements across multiple pages, such as headers, footers, and sidebars. This helps maintain consistency throughout your application and reduces code duplication.
Creating a Layout Component
To create a layout component, you can define a new React component that wraps the content of your pages. Here's an example of a basic layout component:
// components/Layout.js
import React from 'react';
import Header from './Header';
import Footer from './Footer';
const Layout = ({ children }) => {
return (
<div>
<Header />
<main>{children}</main>
<Footer />
</div>
);
};
export default Layout;
In this example, the Layout
component renders a Header
, the page content (passed as the children
prop), and a Footer
. You can further customize the layout by adding additional components or styling as needed.
Applying a Layout to a Page
To apply a layout to a page, you can import and render the layout component within your page component. Here's an example:
// pages/index.js
import React from 'react';
import Layout from '../components/Layout';
const HomePage = () => {
return (
<Layout>
<h1>Welcome to our website!</h1>
{/* Page content */}
</Layout>
);
};
export default HomePage;
In this example, the HomePage
component is wrapped by the Layout
component, ensuring that the header and footer are rendered along with the page content.
Nested Layouts
Next.js 14 also supports nested layouts, which can be useful when you have different layout requirements for different sections of your application. For example, you might have a main layout for your website, and a separate layout for your blog section.
To create nested layouts, you can simply render one layout component inside another. Here's an example:
// components/MainLayout.js
import React from 'react';
import Header from './Header';
import Footer from './Footer';
const MainLayout = ({ children }) => {
return (
<div>
<Header />
<main>{children}</main>
<Footer />
</div>
);
};
export default MainLayout;
// components/BlogLayout.js
import React from 'react';
import MainLayout from './MainLayout';
import Sidebar from './Sidebar';
const BlogLayout = ({ children }) => {
return (
<MainLayout>
<div style={{ display: 'flex' }}>
<Sidebar />
<div style={{ flex: 1 }}>{children}</div>
</div>
</MainLayout>
);
};
export default BlogLayout;
In this example, the BlogLayout
component renders the MainLayout
component, which includes the header and footer. Additionally, the BlogLayout
component adds a sidebar and a container for the page content.
You can then apply the BlogLayout
to your blog pages:
// pages/blog/[slug].js
import React from 'react';
import BlogLayout from '../../components/BlogLayout';
const BlogPostPage = () => {
return (
<BlogLayout>
{/* Blog post content */}
</BlogLayout>
);
};
export default BlogPostPage;
Server Components
Next.js 14 introduces a new type of React component called Server Components. These components are designed to be rendered on the server and streamed to the client, providing improved performance and a more efficient rendering process.
Creating a Server Component
To create a Server Component, you can use the 'use server'
directive at the top of your component file. Here's an example:
// 'use server' directive
import React from 'react';
const ServerComponent = () => {
return (
<div>
<h1>This is a Server Component</h1>
{/* Component content */}
</div>
);
};
export default ServerComponent;
Server Components can be used in both pages and layouts, allowing you to optimize the rendering process and improve the overall performance of your application.
Data Fetching
Next.js 14 provides several options for fetching data in your application, including client-side fetching, server-side fetching, and static data fetching.
Client-side Data Fetching
Client-side data fetching is suitable for situations where the data needs to be loaded dynamically after the initial page render. You can use React's built-in useEffect
hook or a third-party library like react-query
or swr
to fetch data on the client-side.
import React, { useState, useEffect } from 'react';
const ClientDataFetching = () => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
};
fetchData();
}, []);
return (
<div>
{data ? (
<div>
{/* Render data */}
</div>
) : (
<div>Loading...</div>
)}
</div>
);
};
export default ClientDataFetching;
Server-side Data Fetching
Server-side data fetching is useful when you need to fetch data before rendering the page on the server. Next.js provides the getServerSideProps
function, which allows you to fetch data on the server and pass it as props to your page component.
// pages/products.js
import React from 'react';
const ProductsPage = ({ products }) => {
return (
<div>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
};
export const getServerSideProps = async () => {
const response = await fetch('https://api.example.com/products');
const products = await response.json();
return {
props: {
products,
},
};
};
export default ProductsPage;
In this example, the getServerSideProps
function fetches data from an API and passes it as props to the ProductsPage
component, which then renders the products.
Static Data Fetching
Static data fetching is useful for pages that can be pre-rendered at build time. Next.js provides the getStaticProps
and getStaticPaths
functions for this purpose.
// pages/blog/[slug].js
import React from 'react';
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const BlogPostPage = ({ frontmatter, content }) => {
return (
<div>
<h1>{frontmatter.title}</h1>
<div dangerouslySetInnerHTML={{ __html: content }} />
</div>
);
};
export const getStaticPaths = async () => {
const files = fs.readdirSync(path.join('posts'));
const paths = files.map((filename) => ({
params: {
slug: filename.replace('.md', ''),
},
}));
return {
paths,
fallback: false,
};
};
export const getStaticProps = async ({ params: { slug } }) => {
const markdownWithMeta = fs.readFileSync(path.join('posts', slug + '.md'), 'utf-8');
const { data: frontmatter, content } = matter(markdownWithMeta);
return {
props: {
frontmatter,
content,
},
};
};
export default BlogPostPage;
In this example, the getStaticPaths
function generates the list of paths (slugs) for the blog posts. The getStaticProps
function fetches the content and metadata for a specific blog post based on the slug and passes it as props to the BlogPostPage
component.
Incremental Static Regeneration (ISR)
Next.js 14 introduces Incremental Static Regeneration (ISR), which allows you to update static pages after they have been built, providing a balance between the benefits of static and dynamic rendering.
To enable ISR, you can use the revalidate
option in getStaticProps
:
// pages/products/[id].js
export const getStaticProps = async ({ params }) => {
const response = await fetch(`https://api.example.com/products/${params.id}`);
const product = await response.json();
return {
props: {
product,
},
revalidate: 60, // Regenerate the page every 60 seconds
};
};
In this example, the revalidate
option specifies that the page should be regenerated every 60 seconds, ensuring that the data remains up-to-date.
Styling and CSS
Next.js 14 provides several options for styling your application, including CSS Modules, global styles, and importing styles from third-party libraries.
CSS Modules
CSS Modules is a popular approach to scoping CSS styles in Next.js. Each component can have its own CSS file, and the styles are automatically scoped to that component, preventing naming conflicts.
// components/Button.js
import React from 'react';
import styles from './Button.module.css';
const Button = ({ children }) => {
return <button className={styles.button}>{children}</button>;
};
export default Button;
// components/Button.module.css
.button {
background-color: blue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
In this example, the Button
component imports its styles from the Button.module.css
file, and the styles are scoped to that component using the styles
object.
Global Styles
If you need to apply styles globally across your application, you can create a global CSS file and import it into your application.
// pages/_app.js
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
// styles/globals.css
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
In this example, the globals.css
file contains global styles that are applied to the entire application by importing it in the _app.js
file.
Importing Styles from a Third-Party Library
Next.js also allows you to import styles from third-party libraries, such as Bootstrap or Material-UI.
// pages/_app.js
import 'bootstrap/dist/css/bootstrap.min.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
// components/NavBar.js
import React from 'react';
const NavBar = () => {
return (
<nav className="navbar navbar-expand-lg navbar-dark bg-primary">
{/* Navbar content */}
</nav>
);
};
export default NavBar;
In this example, the Bootstrap CSS file is imported in the _app.js
file, allowing you to use Bootstrap classes throughout your application, as demonstrated in the NavBar
component.
Deployment and Performance
After building your Next.js application, you can deploy it to various hosting platforms, such as Vercel, Netlify, or a custom server.
Building and Deploying a Next.js Application
To build your Next.js application for production, run the following command:
Copy codenpm run build
This command will create an optimized production build in the .next
directory.
Next, you can start the production server by running:
Copy codenpm start
This will serve your application from the .next
directory, making it ready for deployment.
Performance Optimizations
Next.js 14 comes with several built-in performance optimizations, including code splitting, image optimization, and more.
Code Splitting
Next.js automatically splits your application code into smaller chunks, allowing for lazy loading of non-critical code and improving the initial load time.
Image Optimization
Next.js provides an optimized Image
component that automatically optimizes images for performance by resizing, compressing, and serving them in modern formats like WebP.
import Image from 'next/image';
const ImageExample = () => {
return (
<Image
src="/images/example.jpg"
alt="Example Image"
width={500}
height={300}
/>
);
};
In this example, the Image
component will automatically optimize the example.jpg
image for performance.
Conclusion
Working with pages and layouts in Next.js 14 has become more powerful and flexible than ever before. With the introduction of Server Components, improved data fetching options, and enhanced performance optimizations, developers can build modern web applications that are fast, efficient, and user-friendly.
As the Next.js ecosystem continues to grow, we can expect even more exciting features and improvements in the future. Whether you're building a simple website or a complex web application, Next.js 14 provides a solid foundation for creating high-performance, server-rendered React applications.
For further learning and staying up-to-date with the latest Next.js developments, you can refer to the official Next.js documentation, join the Next.js community, and explore various online resources and tutorials.