As your application grows, you'll often find yourself needing to group related pages under a common structure or theme. This is where nested routes and layouts in Next.js shine. They allow you to define shared UI elements, like navigation bars, footers, or sidebars, that persist across a set of related pages, enhancing user experience and simplifying your codebase.
Next.js's file-system based routing makes implementing nested routes intuitive. You can create directories within your pages directory to represent nested URL segments. For example, a route like /dashboard/settings can be achieved by creating pages/dashboard/settings.js.
import { useRouter } from 'next/router';
function SettingsPage() {
const router = useRouter();
const { slug } = router.query; // If you had dynamic segments like /dashboard/settings/[slug].js
return (
<div>
<h1>Dashboard Settings</h1>
<p>This is the settings page for the dashboard.</p>
</div>
);
}
export default SettingsPage;Beyond just nesting, the real power comes with defining shared layouts for these nested routes. Next.js provides a flexible way to achieve this using a pattern that leverages component composition.
The common approach is to create a layout component that accepts children and wraps them with shared elements. Then, within your page components that belong to a specific nested route, you'll import and use this layout component.
/* components/DashboardLayout.js */
import Navbar from './Navbar';
import Sidebar from './Sidebar';
export default function DashboardLayout({ children }) {
return (
<div className="dashboard-container">
<Navbar />
<div className="main-content">
<Sidebar />
<main>{children}</main>
</div>
<style jsx>{`
.dashboard-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
display: flex;
flex: 1;
}
main {
flex: 1;
padding: 20px;
}
`}</style>
</div>
);
}/* pages/dashboard/index.js */
import DashboardLayout from '../../components/DashboardLayout';
function DashboardPage() {
return (
<div>
<h2>Welcome to your Dashboard!</h2>
<p>This is the main dashboard overview.</p>
</div>
);
}
DashboardPage.getLayout = function getLayout(page) {
return (
<DashboardLayout>
{page}
</DashboardLayout>
);
};
export default DashboardPage;/* pages/dashboard/settings.js */
import DashboardLayout from '../../components/DashboardLayout';
function SettingsPage() {
return (
<div>
<h2>Dashboard Settings</h2>
<p>Adjust your settings here.</p>
</div>
);
}
SettingsPage.getLayout = function getLayout(page) {
return (
<DashboardLayout>
{page}
</DashboardLayout>
);
};
export default SettingsPage;The getLayout convention is a popular pattern to indicate that a page should be wrapped by a specific layout. You then need to implement a custom _app.js to apply this layout logic globally.
/* pages/_app.js */
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
const getLayout = Component.getLayout ?? ((page) => page);
return getLayout(<Component {...pageProps} />);
}
export default MyApp;This pattern allows for flexible layout composition. You can have multiple layout components and pages can opt into the one they need. It's also possible to have nested layouts, where a page might use a layout that itself uses another layout.
graph TD
A[pages/_app.js] --> B{Component.getLayout ? Component.getLayout(page) : page};
B --> C[Layout Component];
C --> D[Page Component];
B --> D;
When dealing with complex nested routing and layouts, consider the clarity of your file structure and the reusability of your layout components. This approach not only keeps your code organized but also promotes a consistent look and feel across your entire application.