Education
Developer Advocate
Last updated onSep 9, 2024
Last updated onMay 23, 2024
Next.js is a robust React framework that enables developers to easily create server-rendered React applications.By combining server-side rendering (SSR) and static site generation (SSG), Next.js enables better performance, SEO optimization, and faster page loads. It also offers features like API routes, dynamic routing, and incremental static regeneration.
The app directory in Next.js 13 and above is a significant evolution in the framework. It introduces a new way to define the folder structure and organize your Next.js app, making it easier to manage complex projects. The app directory supports server components, client components, and the new app router, providing a more modular and scalable approach to building React applications.
Using the app directory in Next.js offers several advantages:
Improved Organization: The app directory allows for better organization of your project files by colocating components and routes within the same folder. This makes it easier to manage large codebases.
Enhanced Routing: With the app router, you can define nested routes and route groups, enabling more advanced routing patterns. This helps in creating more structured and maintainable applications.
Server Components and Client Components: The app directory supports both server components and react server components, allowing for server-side rendering and client-side rendering within the same application. This hybrid approach improves performance and user experience.
Automatic Code Splitting: The app directory automatically handles code splitting, ensuring that only the necessary JavaScript is loaded for each page. This reduces load times and enhances performance.
Better Error Handling: With features like error boundaries and error components, managing errors becomes more straightforward and efficient.
To get started with the app directory in Next.js, you need to have Node.js installed on your machine. The latest version of Next.js can be installed using the following command:
1npx create-next-app@latest my-next-app --use-app
This command initializes a new Next.js project named my-next-app with the app directory structure enabled.
The introduction of the app directory in Next.js 13 and above brings several key differences compared to the traditional pages directory:
1/pages 2 ├── index.js 3 ├── about.js 4 └── [dynamic].js
1/app 2 ├── layout.js 3 ├── page.js 4 ├── about 5 │ ├── layout.js 6 │ └── page.js 7 └── [dynamic] 8 └── page.js
Pages Directory: Routing in the pages directory is straightforward but lacks the flexibility of nested layouts and advanced routing patterns.
App Directory: The app directory leverages the app router to define nested routes and layouts. This allows you to create complex routing structures with ease, supporting features like nested layouts and parallel routes.
Pages Directory: Primarily supports client-side components and server-side rendering.
App Directory: Enhances the use of server components and react server components, enabling better performance by rendering components on the server and sending only the necessary HTML to the client.
Pages Directory: Data fetching methods like getStaticProps and getServerSideProps are used within individual pages.
App Directory: Data fetching can be done within server components, making it more integrated and easier to manage. The app directory supports a unified approach to fetching data and rendering it.
The app directory in Next.js provides a more intuitive and flexible way to organize your application files. The folder structure is designed to support modular and scalable development, making it easier to manage large codebases. Here’s an overview of the default folder hierarchy:
1/app 2 ├── layout.js 3 ├── page.js 4 ├── about 5 │ ├── layout.js 6 │ └── page.js 7 ├── blog 8 │ ├── [id] 9 │ │ └── page.js 10 ├── api 11 │ └── route.js 12 ├── components 13 │ └── Header.js 14 └── styles 15 └── globals.css
app/: This is the root folder for the app directory. It contains all the routes and components for your application.
layout.js: This file defines the root layout for your application, providing a consistent structure across all pages.
page.js: This file represents the home page or index route of your application.
about/: This folder contains routes and components specific to the "about" page, including its own layout and page components.
blog/: This folder demonstrates dynamic routing with a dynamic segment [id].
api/: This folder is used to define API routes.
components/: This folder contains reusable components used throughout the application.
styles/: This folder holds global CSS files for styling your application.
One of the key benefits of the app directory is the safe colocation of files and components. This means you can place related files and components together within the same directory, making it easier to maintain and understand the project structure.
Improved Organization: By colocating related files, such as components, styles, and data fetching logic, within the same directory, you can keep your project organized and modular.
Easier Maintenance: When related code is grouped together, it becomes easier to make changes and updates without having to navigate through unrelated files scattered across the project.
Enhanced Readability: Developers can quickly understand the purpose and functionality of a directory by looking at its contents, improving code readability and collaboration.
Here’s an example demonstrating the colocation of files and components in the app directory:
1/app 2 ├── about 3 │ ├── layout.js 4 │ └── page.js 5 │ └── AboutHeader.js 6 │ └── about.css 7 ├── blog 8 │ ├── [id] 9 │ │ ├── page.js 10 │ │ └── BlogPost.js 11 │ │ └── blog.css
In this example:
The about folder contains the layout, page component, a specific header component (AboutHeader.js), and styles (about.css) related to the "about" page.
The blog[id]
folder contains the page component for individual blog posts, a specific component (BlogPost.js), and styles (blog.css).
In Next.js, defining routes within the app directory is straightforward and flexible. Each folder inside the app directory corresponds to a route segment, making the folder structure intuitive and easy to manage. For example, the following structure creates routes for the home page, about page, and a dynamic blog post page:
1/app 2 ├── layout.js 3 ├── page.js 4 ├── about 5 │ └── page.js 6 └── blog 7 └── [id] 8 └── page.js
Home Page: Defined by page.js directly inside the app directory.
About Page: Defined by page.js inside the about folder.
Dynamic Blog Post: Defined by page.js inside the [id]
folder within the blog directory, supporting dynamic routing.
Layouts in the app directory are defined using layout.js files. These layout files provide a consistent structure across all pages within a specific directory or subdirectory. Unlike traditional layouts in the pages directory, layouts in the app directory are hierarchical and can nest within each other, allowing for complex nested layouts.
1// app/layout.js 2export default function RootLayout({ children }) { 3 return ( 4 <html lang="en"> 5 <body> 6 <header>My App Header</header> 7 <main>{children}</main> 8 <footer>My App Footer</footer> 9 </body> 10 </html> 11 ); 12} 13 14// app/about/layout.js 15export default function AboutLayout({ children }) { 16 return ( 17 <div> 18 <aside>About Sidebar</aside> 19 <section>{children}</section> 20 </div> 21 ); 22}
In this example, RootLayout provides a header and footer for the entire application, while AboutLayout adds a sidebar for the "about" section.
Dynamic routing is implemented using square brackets in the folder names. Nested layouts are achieved by placing layout.js files within subdirectories. Here's an example structure that supports nested layouts:
1/app 2 ├── layout.js 3 ├── page.js 4 ├── about 5 │ ├── layout.js 6 │ └── page.js 7 └── blog 8 ├── layout.js 9 └── [id] 10 └── page.js
Nested Layouts: layout.js files in root and subdirectories create hierarchical layouts.
Dynamic Routing: [id]
folder inside blog supports dynamic routes for individual blog posts.
The page.js file in the app directory represents a route's main component. Each page.js file corresponds to a specific URL path.
1// app/page.js 2export default function HomePage() { 3 return <h1>Welcome to the Home Page</h1>; 4} 5 6// app/about/page.js 7export default function AboutPage() { 8 return <h1>About Us</h1>; 9}
Layout components, defined in layout.js files, provide structure and consistent design elements across multiple pages.
1// app/layout.js 2export default function RootLayout({ children }) { 3 return ( 4 <html lang="en"> 5 <body>{children}</body> 6 </html> 7 ); 8}
The template.js file allows you to define reusable templates for your pages. This can help standardize the layout and styling of similar pages.
1// app/blog/template.js 2export default function BlogTemplate({ children }) { 3 return ( 4 <article> 5 <header>Blog Header</header> 6 {children} 7 </article> 8 ); 9}
The loading.jsx file is used to handle loading states while data is being fetched or components are being rendered.
1// app/loading.jsx 2export default function Loading() { 3 return <div>Loading...</div>; 4}
Custom error handling can be managed with the error.jsx file. This file catches and displays errors that occur during server-side rendering or client-side navigation.
1// app/error.jsx 2export default function Error({ error }) { 3 return ( 4 <div> 5 <h1>Something went wrong</h1> 6 <p>{error.message}</p> 7 </div> 8 ); 9}
The not-found.jsx file is used to handle 404 errors when a route is not found.
1// app/not-found.jsx 2export default function NotFound() { 3 return ( 4 <div> 5 <h1>404 - Page Not Found</h1> 6 <p>The page you are looking for does not exist.</p> 7 </div> 8 ); 9}
Next.js provides powerful methods for data fetching and rendering, leveraging both server-side rendering (SSR) and static site generation (SSG) to optimize performance and user experience. These methods are integrated seamlessly within the app directory, allowing you to choose the best approach for each page of your application.
SSR renders your pages on each request. This means that the HTML is generated on the server for every request, providing fresh data for each page load. This approach is beneficial for pages that require up-to-date data.
Example of SSR with getServerSideProps:
1// app/blog/[id]/page.js 2export async function getServerSideProps(context) { 3 const { id } = context.params; 4 const res = await fetch(`https://api.example.com/blog/${id}`); 5 const data = await res.json(); 6 7 return { 8 props: { 9 blog: data, 10 }, 11 }; 12} 13 14export default function BlogPage({ blog }) { 15 return ( 16 <div> 17 <h1>{blog.title}</h1> 18 <p>{blog.content}</p> 19 </div> 20 ); 21}
SSG generates the HTML at build time, meaning the content is pre-rendered and served statically. This is ideal for pages with static content or data that doesn't change frequently.
1// app/blog/page.js 2export async function getStaticProps() { 3 const res = await fetch('https://api.example.com/blogs'); 4 const data = await res.json(); 5 6 return { 7 props: { 8 blogs: data, 9 }, 10 revalidate: 10, // Revalidate the data every 10 seconds 11 }; 12} 13 14export default function BlogsPage({ blogs }) { 15 return ( 16 <div> 17 <h1>Blog Posts</h1> 18 <ul> 19 {blogs.map((blog) => ( 20 <li key={blog.id}>{blog.title}</li> 21 ))} 22 </ul> 23 </div> 24 ); 25}
Next.js provides several methods for data fetching, which can be used depending on the use case. These methods are getStaticProps, getServerSideProps, and client-side fetching.
Used for SSG, getStaticProps fetches data at build time. It's ideal for static content and pages that don't need to update frequently.
Used for SSR, getServerSideProps fetches data on each request. It's suitable for pages that require dynamic data and frequent updates.
For data that needs to be fetched upon user interaction or post-initial page load, client-side fetching using React's useEffect hook is appropriate.
Example of client-side fetching:
1import { useEffect, useState } from 'react'; 2 3// app/blog/[id]/page.js 4export default function BlogPage({ id }) { 5 const [blog, setBlog] = useState(null); 6 7 useEffect(() => { 8 async function fetchData() { 9 const res = await fetch(`https://api.example.com/blog/${id}`); 10 const data = await res.json(); 11 setBlog(data); 12 } 13 fetchData(); 14 }, [id]); 15 16 if (!blog) return <div>Loading...</div>; 17 18 return ( 19 <div> 20 <h1>{blog.title}</h1> 21 <p>{blog.content}</p> 22 </div> 23 ); 24}
Internationalization (i18n) is essential for creating applications that can support multiple languages and cater to a global audience. Setting up i18n in the Next.js app directory involves configuring language settings and ensuring that your application can dynamically switch between languages.
Steps to Set Up i18n
1npm install next-i18next react-i18next i18next
1// next-i18next.config.js 2module.exports = { 3 i18n: { 4 locales: ['en', 'de'], 5 defaultLocale: 'en', 6 localePath: './public/locales', 7 }, 8};
Then, ensure this configuration is referenced in your next.config.js:
1// next.config.js 2const { i18n } = require('./next-i18next.config'); 3 4module.exports = { 5 i18n, 6 reactStrictMode: true, 7};
1public 2└── locales 3 ├── en 4 │ └── common.json 5 └── de 6 └── common.json
Example content of public/locales/en/common.json:
1{ 2 "welcome": "Welcome to our application" 3}
1// app/i18n/index.js 2import i18n from 'i18next'; 3import { initReactI18next } from 'react-i18next'; 4import { getOptions } from './settings'; 5 6export function initI18n(lng, ns) { 7 i18n 8 .use(initReactI18next) 9 .init(getOptions(lng, ns)); 10 return i18n; 11}
1// src/pages/_app.js 2import { appWithTranslation } from 'next-i18next'; 3 4function MyApp({ Component, pageProps }) { 5 return <Component {...pageProps} />; 6} 7 8export default appWithTranslation(MyApp);
1// src/pages/index.js 2import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; 3import { useTranslation } from 'react-i18next'; 4 5export default function Home() { 6 const { t } = useTranslation(); 7 8 return ( 9 <div> 10 <h1>{t('welcome')}</h1> 11 </div> 12 ); 13} 14 15export async function getStaticProps({ locale }) { 16 return { 17 props: { 18 ...(await serverSideTranslations(locale, ['common'])), 19 }, 20 }; 21}
Create a component that allows users to switch languages:
1// app/components/LanguageSwitcher.js 2import Link from 'next/link'; 3import { languages } from '../i18n/settings'; 4 5export default function LanguageSwitcher({ currentLanguage }) { 6 return ( 7 <nav> 8 {languages.map((lng) => ( 9 <Link key={lng} href={`/${lng}`}> 10 <a className={lng === currentLanguage ? 'active' : ''}> 11 {lng.toUpperCase()} 12 </a> 13 </Link> 14 ))} 15 </nav> 16 ); 17}
In this example, the languages array is imported from your i18n settings, and a simple navigation bar is created where each language is represented as a link.
Include the language switcher in your layout or specific pages to allow users to change the language dynamically:
1// app/layout.js 2import LanguageSwitcher from './components/LanguageSwitcher'; 3import { useRouter } from 'next/router'; 4 5export default function RootLayout({ children }) { 6 const { locale } = useRouter(); 7 return ( 8 <html lang={locale}> 9 <body> 10 <LanguageSwitcher currentLanguage={locale} /> 11 {children} 12 </body> 13 </html> 14 ); 15}
In this layout component, useRouter from Next.js is used to determine the current locale. This is passed to the LanguageSwitcher component to highlight the active language.
Ensure that dynamic content is translated properly by using the useTranslation hook from react-i18next:
1// app/[lng]/page.js 2import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; 3import { useTranslation } from 'react-i18next'; 4 5export default function HomePage({ params: { lng } }) { 6 const { t } = useTranslation(); 7 return ( 8 <div> 9 <h1>{t('title')}</h1> 10 <p>{t('description')}</p> 11 </div> 12 ); 13} 14 15export async function getStaticProps({ params }) { 16 return { 17 props: { 18 ...(await serverSideTranslations(params.lng, ['common'])), 19 }, 20 }; 21}
In this page component, serverSideTranslations is used to load the appropriate translations during build time. The useTranslation hook provides the t function to translate keys into the current language.
Namespaces: Use namespaces to organize translations into multiple files, which can help manage large sets of translation strings efficiently.
Locale Detection: Configure Next.js to automatically detect and handle locales based on user preferences or URL structure.
Dynamic Routes: Ensure your dynamic routes and paths support localization by properly handling the locale in your route definitions and component logic.
Optimizing your Next.js application is crucial for enhancing performance, user experience, and SEO. Key optimization techniques include optimizing images and fonts, implementing lazy loading and code splitting, and performing bundle analysis and performance monitoring.
Next.js provides built-in support for image optimization, which can significantly improve load times and overall performance. The next/image component automatically optimizes images for different screen sizes and resolutions.
Example usage of the next/image component:
1import Image from 'next/image'; 2 3export default function HomePage() { 4 return ( 5 <div> 6 <h1>Optimized Images in Next.js</h1> 7 <Image 8 src="/images/example.jpg" 9 alt="Example Image" 10 width={500} 11 height={300} 12 /> 13 </div> 14 ); 15}
Key features of next/image include:
Automatic resizing and optimization.
Support for various image formats (JPEG, PNG, WebP).
Lazy loading by default to improve initial page load times.
Optimizing fonts involves minimizing the number of font requests and ensuring efficient loading. Next.js supports font optimization through the next/font package, which allows you to load fonts directly from the CSS.
Example of loading a Google Font:
1import { Inter } from 'next/font/google'; 2 3const inter = Inter({ subsets: ['latin'] }); 4 5export default function HomePage() { 6 return ( 7 <div className={inter.className}> 8 <h1>Optimized Fonts in Next.js</h1> 9 </div> 10 ); 11}
Lazy loading delays the loading of non-critical resources at page load time, improving the performance and speed of the initial load. Next.js enables lazy loading of components using dynamic imports.
Example of lazy loading a component:
1import dynamic from 'next/dynamic'; 2 3const LazyComponent = dynamic(() => import('./components/LazyComponent'), { 4 loading: () => <p>Loading...</p>, 5}); 6 7export default function HomePage() { 8 return ( 9 <div> 10 <h1>Lazy Loading in Next.js</h1> 11 <LazyComponent /> 12 </div> 13 ); 14}
Code splitting allows you to split your code into smaller bundles, which can be loaded on demand. This improves the performance by reducing the amount of JavaScript loaded initially.
Next.js automatically splits code based on dynamic imports, routes, and components. You can further optimize this by ensuring components and libraries are only imported when necessary.
Analyzing your bundle size helps identify and eliminate unnecessary code, resulting in a leaner and faster application. Next.js provides a built-in bundle analyzer plugin that you can enable to visualize your bundle size.
Install the bundle analyzer:
1npm install @next/bundle-analyzer
Configure the analyzer in next.config.js:
1const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 enabled: process.env.ANALYZE === 'true', 3}); 4 5module.exports = withBundleAnalyzer({});
Run the analysis:
1ANALYZE=true npm run build
Monitoring the performance of your application helps identify bottlenecks and areas for improvement. Tools like Google Lighthouse, Web Vitals, and custom performance monitoring tools can provide insights into the performance metrics of your Next.js application.
Example of using Web Vitals:
1import { useEffect } from 'react'; 2import { getCLS, getFID, getLCP } from 'web-vitals'; 3 4function sendToAnalytics(metric) { 5 // Implement your analytics logic here 6 console.log(metric); 7} 8 9export default function HomePage() { 10 useEffect(() => { 11 getCLS(sendToAnalytics); 12 getFID(sendToAnalytics); 13 getLCP(sendToAnalytics); 14 }, []); 15 16 return ( 17 <div> 18 <h1>Performance Monitoring in Next.js</h1> 19 </div> 20 ); 21}
TypeScript enhances your Next.js application by adding static types, which helps catch errors during development and improves code quality. Integrating TypeScript into a Next.js app directory setup is straightforward.
1npm install typescript @types/react @types/node
1npx tsc --init
Next.js will automatically detect and configure the tsconfig.json file.
Example of a TypeScript component:
1// app/page.tsx 2import React from 'react'; 3 4interface Props { 5 title: string; 6} 7 8const HomePage: React.FC<Props> = ({ title }) => { 9 return <h1>{title}</h1>; 10}; 11 12export default HomePage;
Environment variables are essential for storing configuration values, such as API keys, that change based on the environment (development, production, etc.).
Create Environment Files:
.env.local: For local development.
.env.production: For production environment.
.env.development: For development environment.
Access Environment Variables: Prefix variables with NEXT_PUBLIC_ to make them accessible in the browser.
1// .env.local 2NEXT_PUBLIC_API_URL=https://api.example.com
Usage in a component:
1// app/components/ApiComponent.tsx 2const ApiComponent = () => { 3 const apiUrl = process.env.NEXT_PUBLIC_API_URL; 4 5 return <div>API URL: {apiUrl}</div>; 6}; 7 8export default ApiComponent;
Path aliases simplify the import statements by providing a shorthand for long relative paths.
1{ 2 "compilerOptions": { 3 "baseUrl": ".", 4 "paths": { 5 "@components/*": ["app/components/*"], 6 "@styles/*": ["app/styles/*"] 7 } 8 } 9}
1import Header from '@components/Header'; 2import '@styles/globals.css';
Next.js allows you to customize the underlying Webpack configuration to better fit your project's needs.
1const nextConfig = { 2 webpack: (config, { isServer }) => { 3 // Example: Add a custom loader 4 config.module.rules.push({ 5 test: /\.md$/, 6 use: 'raw-loader', 7 }); 8 9 // Example: Modify an existing rule 10 const svgRule = config.module.rules.find(rule => rule.test?.test('.svg')); 11 if (svgRule) { 12 svgRule.use = [ 13 { 14 loader: '@svgr/webpack', 15 options: { icon: true }, 16 }, 17 ]; 18 } 19 20 return config; 21 }, 22}; 23 24module.exports = nextConfig; 25
1const withPlugins = require('next-compose-plugins'); 2const withTM = require('next-transpile-modules')(['some-module']); 3 4module.exports = withPlugins([withTM], nextConfig);
Next.js supports customization of other build tools such as Babel and PostCSS.
1{ 2 "presets": ["next/babel"], 3 "plugins": [["styled-components", { "ssr": true }]] 4}
1module.exports = { 2 plugins: { 3 autoprefixer: {}, 4 'postcss-nested': {}, 5 }, 6};
In summary, the adoption of Next.js' app directory in version 13 and above revolutionizes React application development. With enhanced organization, routing, and support for server components, it streamlines development while boosting performance and user experience. Combined with optimization techniques and advanced customization options, it empowers developers to build efficient, scalable, and immersive web applications.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.