# Code Style and Conventions Standards for Next.js
This document outlines the coding style and conventions standards for Next.js projects. Adhering to these guidelines ensures code consistency, readability, maintainability, and performance. The standards are based on modern Next.js best practices and aim to guide developers in writing high-quality, scalable, and secure applications.
## 1. General Formatting and Style
### 1.1. Code Formatting
* **Do This:** Use a code formatter like Prettier to automatically format your code. Configure it to enforce consistent indentation, line lengths, and spacing.
* **Why:** Automated formatting eliminates stylistic debates, ensures consistent code appearance, and improves readability.
"""json
// .prettierrc.json
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}
"""
* **Don't Do This:** Rely on manual formatting. It's prone to inconsistencies and wastes valuable development time.
### 1.2. Linting
* **Do This:** Integrate ESLint with a recommended Next.js configuration (e.g., "eslint-config-next") and fix all linting errors and warnings.
* **Why:** Linting identifies potential errors, enforces coding standards, and promotes best practices proactively.
"""bash
npm install --save-dev eslint eslint-config-next
"""
"""javascript
// .eslintrc.json
{
"extends": "next/core-web-vitals",
"rules": {
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
"no-unused-vars": "warn",
"react/prop-types": "off"
}
}
"""
* **Don't Do This:** Ignore linting errors or disable rules without a valid reason. This can lead to code quality issues and technical debt.
### 1.3. Indentation and Spacing
* **Do This:** Use 2 spaces for indentation. Consistent indentation improves code readability.
* **Do This:** Use spaces around operators and after commas for better readability.
"""javascript
// Correct
const result = a + b;
const myArray = [1, 2, 3];
// Incorrect
const result=a+b;
const myArray=[1,2,3];
"""
* **Don't Do This:** Use tabs or inconsistent spacing.
### 1.4. Line Length
* **Do This:** Aim for a maximum line length of 100 characters. Break long lines into smaller, more manageable segments.
* **Why:** Shorter lines improve readability, especially on smaller screens or when comparing code side-by-side.
### 1.5. Comments
* **Do This:** Write clear, concise, and meaningful comments to explain complex logic or non-obvious code. Use JSDoc-style comments for documenting functions and components.
* **Do This:** Update comments when the underlying code changes.
* **Don't Do This:** Write obvious comments that merely restate the code. Over-commenting clutters the code and makes it harder to read. Avoid outdated or misleading comments.
"""javascript
/**
* Fetches user data from the API.
* @param {string} userId - The ID of the user to fetch.
* @returns {Promise} - A promise that resolves to the user data.
*/
async function fetchUser(userId) {
// ... implementation details
}
"""
### 1.6. Consistent Style
* **Do This:** Maintain a consistent code style throughout the project. This includes using the same naming conventions, indentation style, comment style, and other formatting rules.
* **Why:** Consistency makes the codebase easier to understand and maintain.
* **Don't Do This:** Allow different code styles to be used in different parts of the project. This can create confusion and make the code harder to read.
## 2. Naming Conventions
### 2.1. Variables and Constants
* **Do This:** Use "camelCase" for variable and function names. Use descriptive and meaningful names. For constants, use "UPPER_SNAKE_CASE".
"""javascript
const userAge = 30;
const MAX_USERS = 1000;
function calculateTotal(items) {
// ...
}
"""
* **Don't Do This:** Use single-letter variable names (except in simple loops) or abbreviations that are unclear.
### 2.2. Components
* **Do This:** Use "PascalCase" for component names. File names should match the component name.
"""javascript
// components/UserProfile.jsx
function UserProfile() {
return User Profile;
}
"""
* **Don't Do This:** Use lowercase or snake_case for component names.
### 2.3. File and Directory Structure
* **Do This:** Organize files and directories logically based on feature or functionality. Use a consistent naming scheme. For example:
"""
/components
/ui
Button.jsx
Card.jsx
/feature
UserProfile.jsx
ProductList.jsx
/pages
index.jsx
/api
users.js
"""
* **Don't Do This:** Dump all files into a single directory or use inconsistent naming schemes.
### 2.4. API Routes
* **Do This:** Name API route files descriptively to indicate their purpose (e.g., "pages/api/users/login.js").
## 3. React and Next.js Specific Conventions
### 3.1. Functional Components
* **Do This:** Prefer functional components with Hooks over class components.
* **Why:** Functional components are generally more concise, easier to test, and promote code reusability with Hooks. They are the modern React standard.
"""javascript
function MyComponent() {
const [count, setCount] = React.useState(0);
return (
<p>Count: {count}</p>
setCount(count + 1)}>Increment
);
}
"""
* **Don't Do This:** Use class components unless there's a specific reason to do so (e.g., integrating with legacy code).
### 3.2. Data Fetching
* **Do This:** Use "getServerSideProps" for data that needs to be fetched on every request. Use "getStaticProps" for data that can be pre-rendered at build time. Use "getStaticPaths" in conjunction with "getStaticProps" for dynamic routes with pre-rendered pages. Utilize the Next.js "fetch" extension for automatic request de-duplication.
* **Why:** Choosing the appropriate data fetching method optimizes performance and SEO. "getServerSideProps" provides up-to-date data, "getStaticProps" offers faster initial load times, and "getStaticPaths" enables pre-rendering for dynamic content.
"""javascript
// pages/users/[id].jsx
export async function getStaticPaths() {
const res = await fetch('https://api.example.com/users');
const users = await res.json();
const paths = users.map((user) => ({
params: { id: user.id.toString() },
}));
return { paths, fallback: false };
}
export async function getStaticProps({ params }) {
const res = await fetch("https://api.example.com/users/${params.id}");
const user = await res.json();
return { props: { user } };
}
function User({ user }) {
return (
{user.name}
<p>{user.email}</p>
);
}
export default User;
"""
* **Don't Do This:** Fetch data in the component's "useEffect" hook unless it's client-side specific data. This can lead to performance issues and SEO problems. Avoid mixing server-side and client-side data fetching unnecessarily.
### 3.3. API Routes
* **Do This:** Use Next.js API routes for creating backend endpoints. Handle different HTTP methods appropriately. Validate and sanitize input data.
"""javascript
// pages/api/users.js
export default async function handler(req, res) {
if (req.method === 'GET') {
// Fetch users from database
res.status(200).json({ users: [] });
} else if (req.method === 'POST') {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ message: 'Missing required fields' });
}
// Save user to database
res.status(201).json({ message: 'User created successfully' });
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end("Method ${req.method} Not Allowed");
}
}
"""
* **Don't Do This:** Perform complex backend logic directly in the frontend components, expose sensitive data directly to the client, or skip input validation.
### 3.4. Environment Variables
* **Do This:** Store sensitive configuration values in environment variables. Use ".env.local" for local development and configure environment variables in your hosting provider for production. Use "next.config.js" to expose environment variables to the client in a controlled way.
"""javascript
// next.config.js
module.exports = {
env: {
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
},
};
// Access in component:
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
"""
* **Don't Do This:** Hardcode sensitive values directly in the code or commit ".env.local" to your repository.
### 3.5. Image Optimization
* **Do This:** Use the "" component from "next/image" for optimizing images. Specify "width", "height", and "alt" attributes. Use "priority" for above-the-fold images to improve LCP. Configure "domains" or "remotePatterns" in "next.config.js" for external images.
* **Why:** "next/image" automatically optimizes images for different devices and screen sizes, improving performance and user experience.
"""javascript
import Image from 'next/image';
function MyComponent() {
return (
);
}
"""
"""javascript
// next.config.js
module.exports = {
images: {
domains: ['example.com'],
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com',
port: '',
pathname: '/account123/**',
},
],
},
};
"""
* **Don't Do This:** Use standard "" tags without optimization or neglect to specify required attributes.
### 3.6. Link Component
* **Do This:** Use the "" component from "next/link" for navigating between pages.
* **Why:** "next/link" provides client-side navigation, improving performance and user experience.
"""javascript
import Link from 'next/link';
function MyComponent() {
return (
About Us
);
}
"""
* **Don't Do This:** Use standard "" tags for internal navigation. This will cause a full page reload.
### 3.7. Metadata and SEO
* **Do This:** Use the "" component from "next/head" to manage document "" elements such as title, meta descriptions, and other SEO-related tags. Utilize the "Metadata" API for Route-based metadata.
* **Why:** Properly configured metadata improves SEO and provides a better user experience in search engine results.
"""javascript
import Head from 'next/head';
function MyComponent() {
return (
{/* Other meta tags */}
// ... component content
);
}
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function Page() {
return Hello, Next.js!
}
"""
* **Don't Do This:** Neglect to provide meaningful "title" and "meta" descriptions.
### 3.8. Error Handling
* **Do This:** Implement robust error handling in API routes and data fetching functions. Use "try...catch" blocks to catch errors and provide informative error messages. Handle errors gracefully in the UI. Utilize Next.js's built-in error page customization.
* **Don't Do This:** Let errors crash the application or expose sensitive error information to the client.
### 3.9. Code Splitting and Dynamic Imports
* **Do This:** Use dynamic imports ("import('module')") to split your code into smaller chunks. This can improve the initial load time of your application.
* **Why:** Code splitting allows the browser to download only the code that is needed for the current page, reducing the initial load time.
"""javascript
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [Component, setComponent] = useState(null);
useEffect(() => {
import('./HeavyComponent')
.then((module) => {
setComponent(() => module.default);
})
.catch((err) => {
console.error("Failed to load component", err);
});
}, []);
if (!Component) {
return <p>Loading...</p>;
}
return ;
}
export default MyComponent;
"""
* **Don't Do This:** Load all of the code upfront, especially for large applications.
### 3.10. Accessibility (a11y)
* **Do This:** Write semantic HTML, provide alternative text for images ("alt" attribute), use proper ARIA attributes, and test your application with assistive technologies like screen readers.
* **Why:** Accessibility ensures that your application is usable by everyone, including people with disabilities.
* **Don't Do This:** Ignore accessibility concerns.
### 3.11. Using the App Router
* **Do This:** When using the "/app" router, take advantage of Server Components for initial rendering and data fetching. Use Client Components for interactive elements requiring browser-side JavaScript. Clearly delineate the boundaries between Server and Client components. Leverage the "use client" directive.
* **Why:** Server Components reduce the amount of JavaScript sent to the client, leading to faster initial load times and improved performance.
"""tsx
// app/page.tsx (Server Component by default)
import { getData } from './data';
export default async function Page() {
const data = await getData();
return (
My Page
<p>{data.content}</p>
);
}
// app/components/ClientComponent.tsx
'use client'; // Marks this as a Client Component
import { useState } from 'react';
export default function ClientComponent() {
const [count, setCount] = useState(0);
return (
<p>Count: {count}</p>
setCount(count + 1)}>Increment
);
}
"""
* **Don't Do This:** Perform client-side data fetching in Server Components or attempt to use client-side Hooks (e.g., "useState", "useEffect") directly in Server Components without marking them as client components, leading to runtime errors.
## 4. Security Considerations
### 4.1. Input Sanitization and Validation
* **Do This:** Sanitize and validate all user inputs to prevent XSS, SQL injection, and other security vulnerabilities.
* **Don't Do This:** Trust user input directly without sanitization.
### 4.2. Cross-Site Scripting (XSS)
* **Do This:** Use appropriate escaping techniques when rendering user-provided data to prevent XSS attacks.
* **Don't Do This:** Dynamically render HTML without proper escaping.
### 4.3. Authentication and Authorization
* **Do This:** Implement secure authentication and authorization mechanisms to protect sensitive data and resources. Use established libraries like NextAuth.js.
* **Don't Do This:** Roll your own authentication system without proper security expertise.
### 4.4. CSRF Protection
* **Do This:** Implement CSRF protection for all state-changing API routes.
* **Don't Do This:** Leave your application vulnerable to CSRF attacks.
## 5. Testing
### 5.1. Unit Tests
* **Do This:** Write unit tests for individual components and functions to ensure they behave as expected.
* **Don't Do This:** Neglect to write unit tests.
### 5.2. Integration Tests
* **Do This:** Write integration tests to verify that different parts of the application work together correctly. Utilize tools like Cypress or Playwright.
* **Don't Do This:** Rely solely on manual testing.
### 5.3. End-to-End (E2E) Tests
* **Do This:** Write end-to-end tests to simulate user interactions and ensure the entire application functions as expected.
* **Don't Do This:** Deploy without any automated testing.
## 6. Conclusion
By adhering to these coding standards and conventions, developers can create high-quality, maintainable, and scalable Next.js applications. This document provides a solid foundation for building robust and secure web applications with Next.js and will be frequently updated to reflect the latest best practices. Remember to stay current with the latest Next.js documentation and community recommendations.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Next.js This document outlines the component design standards for Next.js applications, focusing on creating reusable, maintainable, and performant components optimized for the Next.js ecosystem. These standards are designed to guide developers and inform AI coding assistants. ## 1. Component Architecture and Organization ### 1.1. Atomic Design Principles **Standard:** Adopt Atomic Design principles (Atoms, Molecules, Organisms, Templates, Pages) to promote reusability and modularity. * **Why:** Atomic design allows for a more systematic and scalable approach to UI development. Reusable Atoms and Molecules form the building blocks for more complex Organisms and Templates, leading to consistency and easier maintenance. **Do This:** * Categorize components based on Atomic Design principles. * Create a clear component hierarchy that reflects the UI structure. **Don't Do This:** * Create monolithic components that are difficult to reuse or maintain. * Mix different levels of abstraction within a single component. **Example:** """jsx // Atoms: Button.jsx import React from 'react'; const Button = ({ children, onClick, className = "" }) => ( <button onClick={onClick} className={"px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 ${className}"}> {children} </button> ); export default Button; // Molecules: SearchBar.jsx import React, { useState } from 'react'; import Button from './Button'; const SearchBar = ({ onSearch }) => { const [searchTerm, setSearchTerm] = useState(''); const handleSearch = () => { onSearch(searchTerm); }; return ( <div className="flex"> <input type="text" className="border rounded py-2 px-3 mr-2 focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="Search..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> <Button onClick={handleSearch}>Search</Button> </div> ); }; export default SearchBar; // Organisms : Header.jsx import React from 'react'; import SearchBar from './SearchBar'; import Logo from './Logo'; const Header = ({ onSearch }) =>( <header className="bg-gray-100 p-4 flex items-center justify-between"> <Logo /> <SearchBar onSearch={onSearch} /> </header> ); export default Header; """ ### 1.2. Component Folder Structure **Standard:** Employ a consistent folder structure for organizing components. * **Why:** A well-defined structure improves code discoverability and maintainability, especially in large projects. **Do This:** * Group related components into directories. Consider using a pattern like "components/ComponentName/ComponentName.jsx" alongside "components/ComponentName/ComponentName.module.css" or "components/ComponentName/index.jsx" to export the component. * Place shared or global components in a dedicated directory (e.g., "components/shared"). **Don't Do This:** * Scatter components randomly throughout the project. * Create excessively deep or complex directory structures. **Example:** """ src/ ├── components/ │ ├── Button/ │ │ ├── Button.jsx │ │ └── Button.module.css │ ├── Card/ │ │ ├── Card.jsx │ │ └── Card.module.css │ ├── shared/ // Global components/utilities │ │ ├── Layout.jsx │ │ └── Loader.jsx """ ### 1.3. Container vs. Presentational Components **Standard:** Differentiate between container components (handling logic and data) and presentational components (rendering UI). * **Why:** Separation of concerns makes components more reusable and testable. Container components can be easily swapped out or modified without affecting the UI. **Do This:** * Pass data and callbacks from container components to presentational components as props. * Keep presentational components focused solely on rendering the UI and avoid containing business logic. **Don't Do This:** * Mix data fetching and UI rendering within a single component. * Overcomplicate presentational components with unnecessary logic. **Example:** """jsx // Container Component: UserListContainer.jsx import React, { useState, useEffect } from 'react'; import UserList from './UserList'; const UserListContainer = () => { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchUsers = async () => { try { const response = await fetch('/api/users'); // Next.js API route const data = await response.json(); setUsers(data); setLoading(false); } catch (error) { console.error("Error fetching users:", error); setLoading(false); } }; fetchUsers(); }, []); if (loading) { return <div>Loading users...</div>; } return <UserList users={users} />; }; export default UserListContainer; // Presentational Component: UserList.jsx import React from 'react'; const UserList = ({ users }) => ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); export default UserList; """ ## 2. Component Implementation Details ### 2.1. Functional Components with Hooks **Standard:** Prefer functional components with Hooks (useState, useEffect, useContext) for managing state and side effects. Use React Server Components where applicable (data fetching, server-side rendering). * **Why:** Hooks provide a cleaner and more concise way to handle state and lifecycle functionalities compared to class components. Server Components improve performance by allowing components to render only on the server. **Do This:** * Use "useState" for managing local component state. * Use "useEffect" for performing side effects (data fetching, subscriptions). * Use "useContext" for accessing shared data from a context. * Consider React Server Components for performance-critical or data-intensive components that don't require client-side interactivity. **Don't Do This:** * Use class components unless absolutely necessary for compatibility with older libraries. * Mutate state directly; always use the state setter function provided by "useState". * Overuse "useEffect"; ensure that side effects are necessary and properly cleaned up. **Example:** """jsx // Functional Component with Hooks import React, { useState, useEffect } from 'react'; const Counter = () => { const [count, setCount] = useState(0); useEffect(() => { document.title = "Count: ${count}"; // Optional: Cleanup function (for unmounting) return () => { document.title = 'My App'; }; }, [count]); // Only re-run the effect if count changes return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }; export default Counter; // React Server Component (app directory) // app/components/DataDisplay.jsx import { getDataFromServer } from '../../lib/dataFetching'; //Dummy data function export default async function DataDisplay() { const data = await getDataFromServer(); return ( <div> <h1>Data from Server:</h1> {data.map(item => ( <p key={item.id}>{item.name}</p> ))} </div> ); } //Dummy data function // lib/dataFetching.js export async function getDataFromServer() { //Simulate fetching server data, await new Promise((resolve) => setTimeout(resolve, 1000)); const data = [ { id: 1, name: "Item A" }, { id: 2, name: "Item B" } ]; return data; } """ ### 2.2. Prop Types and Validation **Standard:** Use PropTypes or TypeScript to define and validate component props. * **Why:** Prop validation helps catch errors early during development, improving code reliability and understandability. TypeScript offers even stronger type safety. **Do This:** * Define the expected type, shape, and required status of each prop. * Use defaultProps to provide default values for optional props. * Consider using TypeScript for static type checking, especially in larger projects. **Don't Do This:** * Skip prop validation, especially for complex components or shared components. * Rely solely on runtime checks; utilize TypeScript where possible. **Example (PropTypes):** """jsx import React from 'react'; import PropTypes from 'prop-types'; const Greeting = ({ name, age, isAdult }) => ( <div> Hello, {name}! You are {age} years old. {isAdult ? 'You are an adult.' : 'You are not an adult.'} </div> ); Greeting.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number.isRequired, isAdult: PropTypes.bool }; Greeting.defaultProps = { isAdult: false } export default Greeting; """ **Example (TypeScript):** """tsx import React from 'react'; interface GreetingProps { name: string; age: number; isAdult?: boolean; // Optional prop } const Greeting: React.FC<GreetingProps> = ({ name, age, isAdult = false }) => ( <div> Hello, {name}! You are {age} years old. {isAdult ? 'You are an adult.' : 'You are not an adult.'} </div> ); export default Greeting; """ ### 2.3. Styling Approaches **Standard:** Employ a consistent styling approach (CSS Modules, Styled Components, Tailwind CSS). * **Why:** Consistent styling improves maintainability and reduces conflicts between styles. CSS Modules offer local scoping, Styled Components allow for component-level styling, and Tailwind CSS provides a utility-first approach. **Do This:** * Choose a styling approach that aligns with the project's needs and team's expertise. * Use CSS Modules for local scoping of styles. * Consider Styled Components for dynamic styles or component-specific themes. * Evaluate Tailwind CSS for rapid UI development with a utility-first approach. **Don't Do This:** * Mix multiple styling approaches within the same project without a clear strategy. * Use global CSS without proper scoping, as it can lead to conflicts. * Overuse inline styles, as they are difficult to maintain and override. **Example (CSS Modules):** """jsx // Button.jsx import React from 'react'; import styles from './Button.module.css'; const Button = ({ children, onClick }) => ( <button onClick={onClick} className={styles.button}> {children} </button> ); export default Button; // Button.module.css .button { background-color: blue; color: white; padding: 10px 20px; border-radius: 5px; cursor: pointer; } .button:hover { background-color: darkblue; } """ **Example (Tailwind CSS):** """jsx import React from 'react'; const Button = ({ children, onClick }) => ( <button onClick={onClick} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"> {children} </button> ); export default Button; """ ### 2.4. Handling Events and Callbacks **Standard:** Pass event handlers and callbacks as props to child components. * **Why:** Clear separation of concerns, making components more reusable and predictable. **Do This:** * Define event handlers in parent components that manage the logic. * Pass these handlers as props to child components. * Use arrow functions to bind "this" (if needed) or, preferably, avoid "this" altogether by using functional components. **Don't Do This:** * Define event handlers directly within child components if the logic depends on the parent component's state. * Use inline event handlers that perform complex logic. **Example:** """jsx // Parent Component: Parent.jsx import React, { useState } from 'react'; import Child from './Child'; const Parent = () => { const [message, setMessage] = useState('Hello'); const handleClick = (newMessage) => { setMessage(newMessage); }; return ( <div> <p>Message: {message}</p> <Child onClick={handleClick} /> </div> ); }; export default Parent; // Child Component: Child.jsx import React from 'react'; const Child = ({ onClick }) => ( <button onClick={() => onClick('Goodbye')}>Click Me</button> ); export default Child; """ ### 2.5 Accessibility (a11y) **Standard:** Build components with accessibility in mind, adhering to accessibility guidelines (WCAG). * **Why:** Ensures that your application is usable by everyone, including people with disabilities. Also improves SEO. **Do This:** * Use semantic HTML elements (e.g., "<button>", "<nav>", "<article>"). * Provide alternative text for images ("alt" attribute). * Ensure sufficient color contrast. * Use ARIA attributes to enhance accessibility for dynamic content. * Test components with screen readers. Tools like Axe DevTools. * Use labels with forms **Don't Do This:** * Rely solely on visual cues for conveying information. * Use generic elements (e.g., "<div>", "<span>") for interactive elements without ARIA attributes. * Neglect keyboard navigation. **Example:** """jsx import React from 'react'; const AccessibleButton = ({ children, onClick, ariaLabel }) => ( <button onClick={onClick} aria-label={ariaLabel}> {children} </button> ); export default AccessibleButton; // Example usage <AccessibleButton onClick={() => alert('Clicked!')} ariaLabel="Click to show alert"> Show Alert </AccessibleButton> //Form example <label htmlFor="name">Name:</label> <input type="text" id="name" name="name" /> """ ## 3. Next.js Specific Considerations ### 3.1. Using Next.js "Link" Component **Standard:** Use the "<Link>" component for client-side navigation between pages. * **Why:** The "<Link>" component provides client-side navigation, resulting in faster page transitions. It also Prefetches pages in the background, further improving performance. This is different from standard "<a>" tags. **Do This:** * Wrap "<a>" tags with "<Link>" when navigating between Next.js pages. * "legacyBehavior={true}" when passing custom components to "Link". **Don't Do This:** * Use regular "<a>" tags for internal navigation, as this will trigger a full page reload and negates Next.js's performance optimizations. * Omit the "href" prop. **Example:** """jsx import Link from 'next/link'; const NavLink = ({ href, children }) => ( <Link href={href}> <a>{children}</a> </Link> ); export default NavLink; // Using a custom component <Link href="/blog" legacyBehavior={true}> <CustomButton> Blog </CustomButton> </Link> """ ### 3.2. Component level Data Fetching **Standard:** Choose the appropriate data fetching strategy based on the component's needs: "getServerSideProps", "getStaticProps", or Client-Side Fetching. React Server Components are now a key consideration for data fetching. * **Why:** Next.js offers several data fetching options, each with its own trade-offs in terms of performance and data freshness. Server Components allow you to move data fetching logic to the server and reduce client-side JavaScript. **Do This:** * Use "getServerSideProps" for frequently updated data. * Use "getStaticProps" for data that doesn't change often and can be pre-rendered at build time. Use "revalidate" for incremental static regeneration. * Use client-side fetching with "useEffect" for data that requires user interaction or is specific to the client. * Utilize React Server Components for data fetching directly within components, especially when the data is not interactive. **Don't Do This:** * Overuse "getServerSideProps", as it can increase server load and slow down page load times. * Fetch all data on the client side if it can be pre-rendered. * Mix server-side and client-side data fetching within the same component without a clear reason. **Example ("getStaticProps"):** """jsx // pages/index.js import React from 'react'; export async function getStaticProps() { const response = await fetch('https://jsonplaceholder.typicode.com/posts'); const posts = await response.json(); return { props: { posts, }, revalidate: 10, // Revalidate every 10 seconds in production }; } const HomePage = ({ posts }) => ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); export default HomePage; """ **Example ("getServerSideProps"):** """jsx // pages/profile.js import React from 'react'; export async function getServerSideProps(context) { const { req, res } = context; // Access cookies or headers if needed const response = await fetch('https://api.example.com/user-profile', { headers: { cookie: req.headers.cookie, // Pass cookies to the API }, }); const profile = await response.json(); return { props: { profile, }, }; } const ProfilePage = ({ profile }) => ( <div> <h1>{profile.name}</h1> <p>{profile.email}</p> </div> ); export default ProfilePage; """ ### 3.3. Image Optimization with "next/image" **Standard:** Use the "<Image>" component from "next/image" for optimizing images. * **Why:** The "<Image>" component automatically optimizes images for different devices and screen sizes. It provides features like lazy loading and placeholder blur effects, contributing to faster page load and improved Core Web Vitals scores. **Do This:** * Replace standard "<img>" tags with "<Image>". * Provide "width" and "height" props to prevent layout shift. * Use the "layout" prop to control how the image scales. * Configure image optimization settings in "next.config.js". **Don't Do This:** * Use standard "<img>" tags for images that require optimization. * Omit "width" and "height" props, as this can cause layout shift. **Example:** """jsx import Image from 'next/image'; import myPic from '../public/me.png'; const MyImage = () => ( <Image src={myPic} alt="Picture of the author" width={500} height={500} /> ); export default MyImage; """ ### 3.4. API Routes **Standard:** Use Next.js API routes for building backend functionalities directly within the Next.js application. * **Why:** API routes provide a simple way to create serverless functions that handle API requests, without needing a separate backend server, colocated within your Next.js application. **Do This:** * Create API routes in the "pages/api" directory(or the new "app/api" directory). * Handle requests and responses using "req" and "res" objects. * Use appropriate HTTP methods (GET, POST, PUT, DELETE). * Apply proper error handling. **Don't Do This:** * Perform overly complex or long-running operations within API routes, as they have execution time limits. Consider offloading these tasks to a separate queue or background process. * Expose sensitive information or API keys directly in the code. **Example:** """javascript // pages/api/hello.js export default function handler(req, res) { res.status(200).json({ text: 'Hello' }); } """ ## 4. Performance Optimization ### 4.1. Code Splitting **Standard:** Leverages Next.js's automatic code splitting capabilities, but be mindful to avoid creating large, monolithic components. * **Why:** Code splitting reduces the initial JavaScript bundle size by splitting the application into smaller chunks, improving initial page load time. Next.js does this automatically based on route. **Do This:** * Keep individual components relatively small and focused. * Dynamically import large components or modules that are not needed on initial page load using "next/dynamic". **Don't Do This:** * Create excessively large components that include all the code for a page. * Import all dependencies at the top of the file, even if they are not used initially. **Example (Dynamic Import):** """jsx import dynamic from 'next/dynamic'; const DynamicComponent = dynamic(() => import('./MyComponent'), { loading: () => <p>Loading...</p>, }); const HomePage = () => ( <div> <p>This is the home page</p> <DynamicComponent /> </div> ); export default HomePage; """ ### 4.2. Memoization **Standard:** Use "React.memo" to memoize functional components and prevent unnecessary re-renders. * **Why:** Memoization optimizes performance by preventing components from re-rendering if their props have not changed. **Do This:** * Wrap pure functional components with "React.memo". * Use a custom comparison function as the second argument to "React.memo" if prop comparisons are more complex than simple equality. * Be cautious when using memoization with components that receive frequently changing props, as the comparison overhead may outweigh the benefits. **Don't Do This:** * Memoize components indiscriminately, as the comparison overhead can sometimes be greater than the rendering cost. * Forget to update memoization when props change. **Example:** """jsx import React from 'react'; const MyComponent = ({ name, age }) => { console.log('Rendering MyComponent'); return ( <div> <p>Name: {name}</p> <p>Age: {age}</p> </div> ); }; export default React.memo(MyComponent); """ ### 4.3. Lazy Loading **Standard:** Implement lazy loading for images and other resources that are not immediately visible on the screen. * **Why:** Lazy loading improves initial page load time by deferring the loading of non-critical resources until they are needed. **Do This:** * Use the "loading="lazy"" attribute for "<img>" tags (when not using "<Image>"). * Use a library like "react-lazyload" for lazy loading components. * Combine lazy loading with code splitting to further optimize performance. **Don't Do This:** * Lazy load above-the-fold content, as this can negatively impact the user experience. * Forget to set appropriate "width" and "height" attributes for lazy-loaded images to prevent layout shift. **Example:** """jsx import React from 'react'; import LazyLoad from 'react-lazyload'; const MyComponent = () => ( <div> <LazyLoad placeholder={<div>Loading...</div>}> <img src="image.jpg" alt="My Image" width="600" height="400" /> </LazyLoad> </div> ); export default MyComponent; """ ## 5. Security Considerations ### 5.1. Input Validation **Standard:** Validate all user inputs on both the client-side and server-side. * **Why:** Input validation prevents malicious code from being injected into the application, protecting against cross-site scripting (XSS) attacks and other vulnerabilities. **Do This:** * Use a library like "validator.js" or "yup" for client-side validation. * Implement server-side validation to ensure data integrity, even if client-side validation is bypassed. * Sanitize user inputs to remove potentially harmful characters or code. **Don't Do This:** * Rely solely on client-side validation, as it can be easily bypassed. * Trust user inputs without proper validation and sanitization. ### 5.2. Environment Variables **Standard:** Store sensitive information (API keys, database credentials) in environment variables. * **Why:** Environment variables prevent sensitive information from being exposed in the codebase, which can be a security risk. **Do This:** * Use ".env" files (or platform-specific environment variable settings) to store sensitive information. * Access environment variables using "process.env.VARIABLE_NAME". * Never commit ".env" files to version control. * Use "next.config.js" to expose environment variables to the client-side code selectively. **Don't Do This:** * Hardcode sensitive information directly in the code. * Commit ".env" files to version control. * Expose all environment variables to the client-side code, only expose what is absolutely required. ### 5.3. Cross-Site Scripting (XSS) Prevention **Standard:** Protect against XSS attacks by properly escaping user-generated content and using Content Security Policy (CSP). * **Why:** XSS attacks allow malicious users to inject scripts into the application, potentially stealing user data or performing unauthorized actions. **Do This:** * Escape user-generated content before rendering it to prevent the execution of malicious scripts, for example, using a templating engine with auto-escaping or a library designed for sanitization. * Implement a strict Content Security Policy (CSP) to control the sources from which the browser is allowed to load resources. * Use the "dangerouslySetInnerHTML" prop with caution and only for trusted content. **Don't Do This:** * Render user-generated content directly without escaping or sanitization. * Use a permissive CSP that allows scripts to be loaded from any source. * Trust user-provided HTML content without proper sanitization. By adhering to these component design standards, Next.js developers can create robust, scalable, and maintainable applications that deliver excellent performance and user experiences.
# Core Architecture Standards for Next.js This document outlines the core architectural standards for Next.js projects. Following these guidelines will promote maintainability, scalability, performance, and security within your applications. These standards are based on the latest versions of Next.js and related technologies. ## 1. Fundamental Architectural Patterns Choosing the correct architectural patterns is crucial for long-term project success. These standards advocate for a modular and scalable approach. ### 1.1 Modular Architecture **Standard:** Structure your Next.js application into independent, reusable modules or components. This aligns with Next.js's component-based nature and promotes code reuse, testability, and maintainability. * **Do This:** Organize code into logical modules, each with a specific responsibility. Use "src/" directory at the project root to group features into directories. * **Don't Do This:** Create large, monolithic components that handle multiple unrelated tasks. Avoid tight coupling between different parts of the application. * **Why:** Modularity reduces complexity, making code easier to understand, debug, and modify. It also promotes collaboration among developers. **Example:** """javascript // src/components/ProductCard/ProductCard.jsx import Image from 'next/image'; import Link from 'next/link'; function ProductCard({ product }) { return ( <Link href={"/products/${product.id}"} key={product.id} className="card"> <Image src={product.image} alt={product.name} width={300} height={200} className="card-image"/> <div className="card-body"> <h2>{product.name}</h2> <p>{product.description}</p> <p>Price: ${product.price}</p> </div> </Link> ); } export default ProductCard; """ """javascript // src/pages/index.jsx import ProductCard from '../components/ProductCard/ProductCard'; async function getProducts() { const res = await fetch('https://fakestoreapi.com/products'); // Replace with your API const products = await res.json(); return products; } export default async function Home() { const products = await getProducts(); return ( <div className="container"> <h1>Welcome to the Store</h1> <div className="grid"> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> </div> ); } """ ### 1.2 Layered Architecture **Standard:** Separate your application into distinct layers: Presentation (UI), Application (Business Logic/Orchestration), and Data Access. Follow the principles of Separation of Concerns. * **Do This:** Create separate directories for components (Presentation), services (Application), and data access logic. Use interfaces to define contracts between layers. Isolate API calls within the Data Access layer. * **Don't Do This:** Directly embed data fetching or business logic within UI components. Mix UI concerns with API calls. * **Why:** Layered architecture improves code organization, testability, and maintainability. It allows you to change one layer without affecting others, providing flexibility for future modifications. **Example:** """javascript // src/services/productService.js (Application Layer) import { getProducts } from '../data/productRepository'; // Data Access Layer export async function fetchProducts() { // Business logic, e.g., filtering, sorting, applying discounts const products = await getProducts(); return products.map(product => ({ ...product, discountedPrice: product.price * 0.9, // Apply a 10% discount })); } """ """javascript // src/data/productRepository.js (Data Access Layer) export async function getProducts() { const res = await fetch('https://fakestoreapi.com/products'); // Replace with your API const products = await res.json(); return products; } """ """javascript // src/pages/index.jsx (Presentation Layer) import { fetchProducts } from '../services/productService'; import ProductCard from '../components/ProductCard/ProductCard'; export default async function Home() { const products = await fetchProducts(); return ( <div className="container"> <h1>Welcome to the Store</h1> <div className="grid"> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> </div> ); } """ ### 1.3 Domain-Driven Design (DDD) **Standard:** Align your architecture with your business domain. Model your code based on domain concepts, entities, and value objects. * **Do This:** Identify core domain concepts and create corresponding classes or data structures. Use a ubiquitous language (shared terminology) between developers and domain experts. * **Don't Do This:** Design your application around technical concerns rather than business requirements. Use generic names that don't reflect the domain. * **Why:** DDD results in a more understandable and maintainable codebase that accurately reflects the business. It simplifies communication and ensures the software solves the right problem. **Example:** Imagine an e-commerce application. Instead of just having a generic "Product" object, you might define more specific domain entities, such as "PhysicalProduct" and "DigitalProduct", each with its own properties and behaviors: """javascript // src/domain/PhysicalProduct.js class PhysicalProduct { constructor(id, name, price, weight) { this.id = id; this.name = name; this.price = price; this.weight = weight; } calculateShippingCost(shippingRate) { return this.weight * shippingRate; } } export default PhysicalProduct; """ """javascript // src/domain/DigitalProduct.js class DigitalProduct { constructor(id, name, price, downloadLink) { this.id = id; this.name = name; this.price = price; this.downloadLink = downloadLink; } getDownloadLink() { return this.downloadLink; } } export default DigitalProduct; """ ## 2. Project Structure and Organization A well-defined project structure is essential for navigating and maintaining a Next.js application. ### 2.1 Standard Directory Structure **Standard:** Use a consistent and predictable directory structure. This promotes discoverability and reduces the cognitive load for developers. * **Do This:** Adhere to the following basic structure: """ my-nextjs-app/ ├── src/ │ ├── pages/ # Next.js Pages Router │ ├── app/ # Next.js App Router (if using) │ ├── components/ # Reusable UI components │ ├── services/ # Business logic services │ ├── data/ # Data access logic │ ├── models/ # Data models / Typescript interfaces │ ├── utils/ # Utility functions │ ├── styles/ # Global styles and component-specific styles │ ├── context/ # React Context providers │ └── lib/ # Helper functions and libraries ├── public/ # Static assets (images, fonts, etc.) ├── next.config.js # Next.js configuration ├── package.json # Project dependencies and scripts └── README.md """ * **Don't Do This:** Randomly place files and folders without a clear organizational principle. Mix different types of code within the same directory. * **Why:** A consistent structure makes it easy to find specific files and understand the application's overall organization. New developers can quickly grasp the project's layout. ### 2.2 Component Organization **Standard:** Organize UI components within the "components/" directory based on their function or domain. * **Do This:** Group related components into subdirectories. For example: """ src/components/ ├── ProductList/ │ ├── ProductList.jsx │ └── ProductItem.jsx ├── ProductCard/ │ └── ProductCard.jsx ├── Layout/ │ ├── Layout.jsx │ ├── Navbar.jsx │ └── Footer.jsx └── UI/ # Generic UI elements ├── Button.jsx └── Input.jsx """ * **Don't Do This:** Keep all components in a single flat list within the "components/" directory. Create deeply nested directory structures that are difficult to navigate. * **Why:** Logical component organization makes it easier to find, reuse, and maintain UI elements. Improves component discoverability. ### 2.3 Utilizing the "app" Router (Next.js 13+) **Standard**: When using Next.js 13 (or later) and embracing the *app* router, structure the "app" directory according to route segments, layout conventions, and server/client component distinctions. * **Do This**: * Use folders to define routes ("app/products/[id]/page.jsx" represents the "/products/:id" route). * Leverage "layout.jsx" files for shared UI across segments. * Clearly mark server components (default) and client components ("'use client'"). * Use "loading.jsx" for suspense boundaries and loading states. * Use "error.jsx" for error handling. * Utilize "route.jsx" or "route.ts" for defining API routes. * Consider using "middleware.ts" or "middleware.jsx" for request pre-processing. * **Don't Do This**: * Mix server and client component logic without clear separation. * Overcomplicate layouts with excessive nesting. * Neglect error handling or loading states. * Rely excessively on client-side data fetching when server components are more appropriate. * **Why**: The "app" router encourages a more declarative, server-centric approach to building Next.js applications. It streamlines data fetching, improves performance, and simplifies routing. Clear separation of server and client components is essential for predictable behavior. **Example:** """ app/ ├── products/ │ ├── [id]/ │ │ ├── page.jsx // Product detail page (Server Component) │ │ ├── loading.jsx // Loading state for the product detail │ │ └── layout.jsx // Layout specific to product pages │ ├── page.jsx // Product list page (Server Component) ├── api/ │ ├── products/ │ │ └── route.js // API route to fetch products ├── layout.jsx // Root layout for the entire application ├── page.jsx // Home page (Server Component) """ """javascript // app/products/[id]/page.jsx (Server Component) import { getProduct } from '@/lib/data'; // Data fetching utility export default async function ProductPage({ params }) { const product = await getProduct(params.id); if (!product) { return <div>Product not found</div>; } return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <p>Price: ${product.price}</p> </div> ); } """ ## 3. Data Fetching Strategies Choosing the right data fetching strategy can significantly impact performance and SEO. The method you use depends on whether you are using the pages directory or the app directory router. ### 3.1 Server-Side Rendering (SSR) **Standard:** Use SSR for pages that require up-to-date data or are important for SEO. * **Do This:** In the "pages" directory, use "getServerSideProps" to fetch data on each request. """javascript // pages/products/[id].jsx export async function getServerSideProps(context) { const { id } = context.params; const res = await fetch("https://fakestoreapi.com/products/${id}"); // Replace with your API const product = await res.json(); return { props: { product, }, }; } function ProductPage({ product }) { return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> </div> ); } export default ProductPage; """ In the "app" directory, just fetch the data directly in the server component. """javascript // app/products/[id]/page.jsx (Server Component) import { getProduct } from '@/lib/data'; // Data fetching utility export default async function ProductPage({ params }) { const product = await getProduct(params.id); if (!product) { return <div>Product not found</div>; } return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> </div> ); } """ * **Don't Do This:** Use client-side data fetching for content that should be indexed by search engines. Overuse SSR for static content. * **Why:** SSR ensures that search engines can crawl and index the content, improving SEO. It also guarantees that users always see the latest data. ### 3.2 Static Site Generation (SSG) **Standard:** Use SSG for pages with content that doesn't change frequently. * **Do This:** In the "pages" directory, use "getStaticProps" to fetch data at build time. Use "getStaticPaths" to pre-render dynamic routes. """javascript // pages/products/[id].jsx export async function getStaticPaths() { const res = await fetch('https://fakestoreapi.com/products'); // Replace with your API const products = await res.json(); const paths = products.map(product => ({ params: { id: product.id.toString() }, })); return { paths, fallback: false, // or 'blocking' }; } export async function getStaticProps(context) { const { id } = context.params; const res = await fetch("https://fakestoreapi.com/products/${id}"); // Replace with your API const product = await res.json(); return { props: { product, }, }; } function ProductPage({ product }) { return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> </div> ); } export default ProductPage; """ In the "app" directory, static generation is the default for server components, so you don't need special methods. * **Don't Do This:** Use SSG for pages with frequently changing data. Forget to define "getStaticPaths" for dynamic routes. * **Why:** SSG provides excellent performance because the pages are pre-rendered at build time and served from a CDN. It reduces server load and improves Time to First Byte (TTFB). ### 3.3 Client-Side Data Fetching (CSR) **Standard:** When using the "pages" router, use CSR for interactive features or content that is personalized to the user. However, minimize CSR and favor SSR/SSG in the "app" directory. * **Do This:** Use libraries like "swr" or "react-query" to manage client-side data fetching and caching in the pages router. In the "app" router, use server components as frequently as possible. """javascript // pages/profile.jsx import useSWR from 'swr'; const fetcher = (url) => fetch(url).then((res) => res.json()); function Profile() { const { data, error } = useSWR('/api/user', fetcher); if (error) return <div>Failed to load user</div>; if (!data) return <div>Loading...</div>; return ( <div> <h1>{data.name}</h1> <p>{data.email}</p> </div> ); } export default Profile; """ When using the app router, use "use client" to mark a client component and then use the "useEffect" hook or a library like "swr" or "react-query" inside of this component. """javascript // app/components/profile.jsx 'use client'; import useSWR from 'swr'; import { useEffect, useState } from 'react'; const fetcher = (url) => fetch(url).then((res) => res.json()); function Profile() { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); const { data, error } = useSWR('/api/user', fetcher); if (error) return <div>Failed to load user</div>; if (!data) return <div>Loading...</div>; return ( <div> <h1>{data.name}</h1> <p>{data.email}</p> </div> ); } export default Profile; """ * **Don't Do This:** Rely on CSR for critical content that needs to be indexed by search engines. Use CSR without proper error handling or loading indicators. * **Why:** CSR allows for dynamic and interactive user interfaces. Data can be fetched and updated in real time without requiring a full page reload. *Using the "app" directory aims to minimize CSR where it makes sense to use server components. ### 3.4 Incremental Static Regeneration (ISR) **Standard:** Use ISR for pages that need to be updated periodically without rebuilding the entire site. Only applicable to the "pages" directory. SSG with on-demand revalidation is the new favored method of regenerating data, available in the "app" directory. * **Do This:** Add a "revalidate" key to "getStaticProps" to specify how often Next.js should regenerate the page in the background. """javascript // pages/products.jsx export async function getStaticProps() { const res = await fetch('https://fakestoreapi.com/products'); // Replace with your API const products = await res.json(); return { props: { products, }, revalidate: 60, // Regenerate every 60 seconds }; } function ProductList({ products }) { return ( <ul> {products.map(product => ( <li key={product.id}>{product.name}</li> ))} </ul> ); } export default ProductList; """ * **Don't Do This:** Use ISR with excessively short revalidation intervals, as this can increase server load. * **Why:** ISR provides a balance between SSG and SSR. Pages are pre-rendered for performance, but they are also updated periodically to reflect changes in the data. This helps optimize performance and use the most up-to-date data possible. Note - this strategy is not available using the new "app" directory. * SSG with on-demand revalidation replaces this functionality for the "app" directory. ### 3.5 Server Actions ("app" router) **Standard:** Leverage Server Actions for handling form submissions and data mutations directly on the server, bypassing the need for a separate API layer in many cases. * **Do This:** Define asynchronous functions marked with "use server" inside a server component or in a dedicated file. Pass these actions directly to form elements or event handlers in client components. * **Don't Do This:** Perform complex business logic or data transformations directly within client components. Avoid unnecessary client-side state updates when the server can handle the operation. * **Why:** Server Actions simplify data handling, reduce client-side JavaScript, and can improve security by keeping sensitive logic on the server. They also integrate seamlessly with React Suspense for improved user experience. **Example:** """javascript // app/actions.js 'use server' import { revalidatePath } from 'next/cache'; import { saveProduct } from './lib/data'; export async function createProduct(formData) { const rawFormData = { name: formData.get('name'), price: parseFloat(formData.get('price')), }; await saveProduct(rawFormData); revalidatePath('/products'); // Invalidate the cache for the products page } """ """javascript // app/products/page.jsx import { createProduct } from '../actions'; export default function ProductsPage() { return ( <form action={createProduct}> <input type="text" name="name" placeholder="Product Name" /> <input type="number" name="price" placeholder="Price"/> <button type="submit">Add Product</button> </form> ); } """ ## 4. Common Anti-Patterns and Mistakes Avoiding common mistakes is equally important as following best practices. * **Over-fetching data:** Only fetch the data that is required for a particular component or page. Use GraphQL or similar technologies to precisely specify data requirements. * **Tight coupling:** Minimize dependencies between different parts of the application. Use interfaces and abstraction layers to decouple components and services. * **Ignoring performance:** Pay attention to page load times, rendering performance, and data fetching efficiency. Use tools like Lighthouse and the Next.js Profiler to identify bottlenecks and optimize performance. * **Lack of error handling:** Implement robust error handling to gracefully handle unexpected errors. Use error boundaries to prevent errors from crashing the entire application. * **Not using environment variables:** Store sensitive information, such as API keys and database credentials, using environment variables. Avoid hardcoding these values directly in the code. * **Neglecting accessibility:** Ensure your application is accessible to users with disabilities. Follow accessibility guidelines (WCAG) and use assistive technologies for testing. * **Skipping Typescript:** For complex applications, strongly consider using Typescript for static typing. This helps catch errors early and improves code maintainability. By adhering to these Core Architecture Standards, your Next.js projects will be better organized, more maintainable, and deliver a superior user experience. Regularly review and update these standards to keep pace with the evolving Next.js ecosystem.
# State Management Standards for Next.js This document outlines the recommended state management standards for Next.js applications. Following these guidelines will lead to more maintainable, performant, and scalable applications. These standards are tailored to contemporary Next.js development and incorporate recent features and best practices. ## 1. Introduction to State Management in Next.js State management in Next.js refers to the strategies and tools used to handle application data, track changes, and ensure reactivity within your components. Because Next.js is a React framework, state management solutions built for React are naturally compatible. However, the unique features of Next.js, such as server-side rendering (SSR), static site generation (SSG), middleware, and the App Router, necessitate tailored approaches. ### 1.1. Why State Management Matters in Next.js * **Data Consistency:** Enforces a single source of truth, reducing discrepancies between different components. * **Improved Performance:** Optimizes re-renders and data fetching strategies. * **Enhanced Maintainability:** Promotes a clear separation of concerns, simplifying debugging and code organization. * **Scalability:** Enables predictable data flows to manage complex applications as they grow. * **SSR/SSG Compatibility:** Ensures data is available both server-side and client-side. ### 1.2 Core Strategies * **Component State (useState, useReducer):** Direct state management within components for local UI concerns. Suited for simple, localized state. * **Context API:** Sharing state across components without prop drilling. Effective for theming, authentication, and other global, relatively static data. * **Server-Side Data Fetching:** Leveraging "getServerSideProps", "getStaticProps", and now React Server Components with "async" components to fetch data on the server. This is primarily for initial page data rather than frequently changing application state. * **Client-Side State Management Libraries (e.g., Zustand, Jotai, Recoil):** More robust solutions for complex state management needs, bringing features such as centralized stores, selectors, and optimized re-renders. * **Caching (e.g. React Query, SWR):** Useful for server-state management. Improves performance by caching fetched data and providing features like automatic background revalidation. * **URL State:** Storing state in the URL using "useRouter" from "next/navigation", suitable for filters, sorting, and pagination. * **Cookies/LocalStorage (with careful consideration):** Can be used for persisting user preferences or session data, but carefully manage synchronization with server-side data. ## 2. Core Standards & Guidelines ### 2.1. Component State (useState, useReducer) **Do This:** * Use "useState" for simple, localized state within a single component. * Use "useReducer" when state logic becomes more complex or involves multiple related pieces of state. * Keep component state focused on UI-related concerns. **Don't Do This:** * Overuse component state for everything. Avoid prop drilling by choosing context, or dedicated state management libraries for data needed in multiple places. * Mutate state directly. Always use the setter function provided by "useState" or dispatch actions within "useReducer". **Why:** Component state is the simplest form of state management, but it can lead to performance issues and code duplication if overused. "useReducer" centralizes state update logic, improving maintainability. **Example ("useState"):** """jsx "use client"; // Mark as client component for interactivity import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } export default Counter; """ **Example ("useReducer"):** """jsx "use client"; // Mark as client component for interactivity import React, { useReducer } from 'react'; const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: 'increment' })}>Increment</button> <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button> </div> ); } export default Counter; """ ### 2.2. Context API **Do This:** * Use Context API for sharing global state, such as theme settings, user authentication, or feature flags. It's best for data that doesn't change too often. * Create a provider component at the root of your application or within specific subtrees. * Use "useContext" to access the context value within consuming components. **Don't Do This:** * Overuse Context API for frequently updated state. Context re-renders all consuming components on any state change, which can lead to performance bottlenecks. Consider a dedicated state management library in such cases. * Store complex state management logic directly within the context provider. Delegate complex logic to custom hooks or reducers. **Why:** Context API avoids prop drilling, but overuse can lead to performance issues due to unnecessary re-renders. Use wisely for specific global state scenarios. **Example:** """jsx // ThemeContext.jsx "use client"; // Mark as client component for interactivity import React, { createContext, useState, useContext } from 'react'; const ThemeContext = createContext(); export function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; const value = { theme, toggleTheme }; return ( <ThemeContext.Provider value={value}> {children} </ThemeContext.Provider> ); } export function useTheme() { return useContext(ThemeContext); } // Component using the context // MyComponent.jsx import React from 'react'; import { useTheme } from './ThemeContext'; function MyComponent() { const { theme, toggleTheme } = useTheme(); return ( <div style={{ backgroundColor: theme === 'light' ? 'white' : 'black', color: theme === 'light' ? 'black' : 'white' }}> <p>Current Theme: {theme}</p> <button onClick={toggleTheme}>Toggle Theme</button> </div> ); } export default MyComponent; // In _app.js or layout.js // app/layout.js import { ThemeProvider } from './ThemeContext'; import MyComponent from './MyComponent'; export default function RootLayout({ children }) { return ( <html lang="en"> <body> <ThemeProvider> <MyComponent /> {children} </ThemeProvider> </body> </html> ); } """ ### 2.3. Server-Side Data Fetching (SSR, SSG, Server Components) **Do This:** * Use "getServerSideProps" (for Pages Router) or "async" Server Components (for App Router) for frequently updated data that requires SEO. * Use "getStaticProps" (for Pages Router) for data that is fetched at build time and doesn't change often, such as blog posts or product catalogs. Ensure you use "revalidate" to periodically update the static data. * Use Incremental Static Regeneration (ISR) with "getStaticProps" to rebuild specific pages after a defined interval without rebuilding the entire site. * Use Server Actions for handling form submissions and mutations directly on the server. * Use "fetch" with "cache: 'force-cache'" (default) or "cache: 'no-store'" options within Server Components to control caching behavior. **Don't Do This:** * Perform client-side data fetching for initial page load if server-side fetching can be used. This degrades user experience and negatively impacts SEO. * Overuse "getServerSideProps" for static content; favor "getStaticProps" with revalidation for better performance. * Store sensitive information directly in the URL or in client-side code. **Why:** Server-side fetching improves SEO and initial load time by delivering pre-rendered HTML. Strategic caching optimizes data retrieval and reduces server load. Server Components move data fetching and other logic to the server, improving client-side performance. **Example ("getServerSideProps" - Pages Router):** """jsx // pages/products/[id].js export async function getServerSideProps(context) { const { id } = context.params; const res = await fetch("https://api.example.com/products/${id}"); const product = await res.json(); return { props: { product, }, }; } function ProductPage({ product }) { return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> </div> ); } export default ProductPage; """ **Example ("getStaticProps" with ISR - Pages Router):** """jsx // pages/blog/[id].js export async function getStaticPaths() { // Call an external API endpoint to get posts const res = await fetch('https://api.example.com/posts') const posts = await res.json() // Get the paths we want to pre-render based on posts const paths = posts.map((post) => ({ params: { id: post.id }, })) // We'll pre-render only these paths at build time. // { fallback: 'blocking' } will server-render pages // on-demand if the path doesn't exist. return { paths, fallback: 'blocking' } } export async function getStaticProps({ params }) { const { id } = params; const res = await fetch("https://api.example.com/posts/${id}"); const post = await res.json(); return { props: { post, }, revalidate: 60, // Revalidate every 60 seconds }; } function PostPage({ post }) { return ( <div> <h1>{post.title}</h1> <p>{post.content}</p> </div> ); } export default PostPage; """ **Example (Server Component with "fetch" - App Router):** """jsx // app/products/[id]/page.js async function getProduct(id) { const res = await fetch("https://api.example.com/products/${id}", { cache: 'no-store' }); // Or 'force-cache' const product = await res.json(); return product; } export default async function ProductPage({ params }) { const product = await getProduct(params.id); return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> </div> ); } """ **Example (Server Actions - App Router):** """jsx // app/actions.js 'use server'; import { revalidatePath } from 'next/cache'; export async function addProduct(formData) { // Simulate database call const product = { name: formData.get('name'), description: formData.get('description'), }; console.log('Adding product:', product); // Invalidate the cache for the product list page revalidatePath('/products'); return { message: 'Product added!' }; } // app/products/page.js import { addProduct } from './actions'; export default function ProductsPage() { return ( <form action={addProduct}> <label htmlFor="name">Name:</label> <input type="text" id="name" name="name" required /> <label htmlFor="description">Description:</label> <textarea id="description" name="description" required /> <button type="submit">Add Product</button> </form> ); } """ ### 2.4. Client-Side State Management Libraries **Do This:** * Select a suitable state management library (Zustand, Jotai, Recoil, Redux Toolkit) based on project needs. * Use libraries like Zustand or Jotai for simpler global state needs and smaller bundle sizes. * Use Recoil for complex, fine-grained state management, particularly when dealing with derived state and granular updates. * Use Redux Toolkit for larger applications with complex state logic and where a more structured, predictable state management pattern is desired. **Don't Do This:** * Introduce a state management library prematurely for small applications. Start with component state or Context API when appropriate. * Mix different state management paradigms without a clear understanding. Choose **one** primary approach for consistency. **Why:** These libraries ensure efficient state updates, centralized management, and better organization for complex applications. **Example (Zustand - App Router, client component):** """jsx // store.js import { create } from 'zustand'; const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), })); export default useStore; // components/Counter.jsx "use client"; import React from 'react'; import useStore from '../store'; function Counter() { const { count, increment, decrement } = useStore(); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </div> ); } export default Counter; """ **Example (Jotai - App Router, client component):** """jsx // atoms.js import { atom } from 'jotai'; export const countAtom = atom(0); // components/Counter.jsx "use client"; import React from 'react'; import { useAtom } from 'jotai'; import { countAtom } from '../atoms'; function Counter() { const [count, setCount] = useAtom(countAtom); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> <button onClick={() => setCount(count - 1)}>Decrement</button> </div> ); } export default Counter; """ **Example (Recoil - App Router, client component):** """jsx // atoms.js import { atom } from 'recoil'; export const countState = atom({ key: 'countState', default: 0, }); // components/Counter.jsx "use client"; import React from 'react'; import { useRecoilState } from 'recoil'; import { countState } from '../atoms'; import { RecoilRoot } from 'recoil'; //Recoil needs to be wrapped function Counter() { const [count, setCount] = useRecoilState(countState); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> <button onClick={() => setCount(count - 1)}>Decrement</button> </div> ); } function AppWrapper() { return ( <RecoilRoot> <Counter /> </RecoilRoot> ); } export default AppWrapper; """ ### 2.5. Caching (React Query/SWR) **Do This:** * Use React Query or SWR for caching server-state, especially data fetched from APIs. * Configure appropriate cache times and revalidation strategies based on data volatility. * Utilize features like optimistic updates and background revalidation for smoother user experiences. **Don't Do This:** * Manually implement caching logic when React Query or SWR can handle it more efficiently. * Cache sensitive data without proper security measures. Handle authentication and authorization correctly. **Why:** Caching significantly improves performance by reducing API calls, while background revalidation ensures data remains fresh. **Example (React Query - App Router, client component):** """jsx "use client"; import React from 'react'; import { useQuery } from '@tanstack/react-query'; async function fetchPosts() { const res = await fetch('https://api.example.com/posts'); return res.json(); } function Posts() { const { data, isLoading, error } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, staleTime: 60000, // 60 seconds }); if (isLoading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <ul> {data.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); } export default Posts; """ **Example (SWR - App Router, client component):** """jsx "use client"; import React from 'react'; import useSWR from 'swr'; const fetcher = (...args) => fetch(...args).then(res => res.json()); function Posts() { const { data, error } = useSWR('https://api.example.com/posts', fetcher, { revalidateIfStale: false, revalidateOnFocus: false, revalidateOnReconnect: false }); if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> return ( <ul> {data.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); } export default Posts; """ ### 2.6. URL State **Do This:** * Use "useRouter" to read and update URL parameters for features like filtering, sorting, and pagination. * Parse and serialize data correctly when storing complex state in the URL. * Use "URLSearchParams" to construct and modify URL parameters efficiently. **Don't Do This:** * Store large amounts of data in the URL. URLs have length limits. * Store sensitive information in the URL. **Why:** URL state allows for bookmarkable and shareable application states. **Example (App Router):** """jsx "use client"; import React from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; function ProductList() { const router = useRouter(); const searchParams = useSearchParams(); const filter = searchParams.get('filter') || 'all'; const handleFilterChange = (newFilter) => { const newSearchParams = new URLSearchParams(searchParams.toString()); newSearchParams.set('filter', newFilter); router.push("?${newSearchParams.toString()}"); }; return ( <div> <select value={filter} onChange={(e) => handleFilterChange(e.target.value)}> <option value="all">All</option> <option value="available">Available</option> <option value="discounted">Discounted</option> </select> {/* Product Listing based on Filter */} </div> ); } export default ProductList; """ ### 2.7. Cookies/LocalStorage **Do This:** * Use cookies or "localStorage" sparingly for persisting user preferences or session data. * Use Javascript libraries like "js-cookie" for easier cookie management. * Prefer cookies for server-side access to data using "cookies()" function in Server Components (App Router) and "req.cookies" in "getServerSideProps" (Pages Router). * Ensure data stored is non-sensitive and consider using appropriate security flags for cookies (e.g., "httpOnly", "secure"). **Don't Do This:** * Store large amounts of data in cookies or "localStorage". There are size limits, and it can affect performance. * Store sensitive data like passwords or API keys. * Rely on cookies/localStorage as the single source of truth for critical application data. They can be cleared by the user. **Why:** Cookies and "localStorage" enable persistence across sessions, but require diligence in terms of storage capacity, security, and synchronization. **Example (Cookies - App Router Server Component):** """jsx // app/page.js (Server Component) import { cookies } from 'next/headers'; export default function HomePage() { const theme = cookies().get('theme')?.value || 'light'; return ( <div> <h1>Welcome</h1> <p>Theme: {theme}</p> {/* ... */} </div> ); } // app/actions.js (Server Action to update cookie) 'use server'; import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; export async function setTheme(theme) { cookies().set('theme', theme); redirect('/'); } """ ## 3. Anti-Patterns to Avoid * **Prop Drilling:** Passing state down multiple levels of components that don't need it. Use Context API or a state management library instead. * **Global Mutable State:** Directly mutating global variables, leading to unpredictable behavior. Use controlled state management with explicit updates. * **Inconsistent Data Fetching:** Mixing server-side and client-side fetching without a clear strategy. Decide on an approach that optimizes performance and SEO. * **Ignoring Caching Opportunities:** Not leveraging caching mechanisms in React Query or SWR, or failing to use ISR, leading to unnecessary API requests. * **Over-Reliance on "useEffect" for Data Fetching:** Using "useEffect" in client components for initial data fetching when server-side solutions are more appropriate for the initial render. * **Storing Sensitive Data Client-Side:** Storing sensitive information in localStorage or cookies without proper encryption or security considerations. ## 4. Performance Considerations * **Minimize Re-renders:** Ensure components only re-render when necessary by using "React.memo", "useMemo", and "useCallback". Optimize context providers by breaking down large contexts into smaller, more specific contexts. * **Code Splitting:** Break down your application into smaller chunks to reduce initial load time. Next.js handles this automatically with its routing system, but be mindful of large component files. * **Optimize Data Fetching:** Use "getServerSideProps", "getStaticProps", or Server Components strategically to fetch data on the server. * **Use Caching:** Leverage caching mechanisms provided by React Query, SWR, or by configuring "fetch" options in Server Components. * **Lazy Load Components:** Utilize "next/dynamic" to lazily load components only when they are needed. ## 5. Conclusion Adhering to these state management standards will contribute to building robust, scalable, and maintainable Next.js applications. By choosing the right tools and techniques for the specific needs of your project, you can optimize performance, improve developer experience, and deliver exceptional user experiences. Continuously review and adapt these standards as the Next.js ecosystem evolves.
# Performance Optimization Standards for Next.js This document outlines the coding standards specifically focused on **performance optimization** for Next.js applications. Adhering to these guidelines contributes to faster, more responsive, and resource-efficient applications, improving user experience and overall system scalability. These standards are designed for the latest versions of Next.js and incorporate modern patterns. ## 1. Data Fetching ### 1.1. Choosing the Right Data Fetching Strategy **Standard:** Select the optimal data fetching method based on the data's volatility and the component's rendering context. * **Do This:** * Use Server Components for data fetching when possible, leveraging the "async/await" syntax for cleaner code. This is the **preferred method** for data fetching in Next.js 13 and above. * Use "fetch" with strategic caching options ("revalidate", "force-cache", "no-store", "revalidatePath", "revalidateTag") within Server Components to manage data freshness. * Employ Client Components with caution when data fetching directly, as they introduce client-side waterfalls. Use primarily for interactive elements that require browser APIs. * Consider using a data fetching library like SWR or React Query for client-side data management, especially for complex caching and invalidation strategies. * For data that needs to be statically generated at build time, use "getStaticProps" (still relevant for the "pages" directory). Understand it's now largely superseded by Server Components for dynamic content. * Use "getServerSideProps" only when data *must* be fetched on every request. Prefer Server Components and caching strategies as a first resort. * **Don't Do This:** * Avoid unnecessary "getServerSideProps" calls, as they bypass the Next.js caching mechanisms. * Avoid fetching data directly in top-level Client Components if possible. * Overuse "getStaticProps" for frequently changing data. * Blindly use one data-fetching method without considering the data's characteristics. **Why:** Choosing the right data fetching strategy dramatically impacts initial load time and subsequent updates. Server Components, combined with strategic caching, provide the best performance for the majority of use cases. **Example (Server Component with Caching):** """jsx // app/products/page.js import { unstable_noStore as noStore } from 'next/cache'; // Opt-out of caching for dynamic content async function getProducts() { // noStore(); // Ensure data is always fresh (use sparingly) const res = await fetch('https://api.example.com/products', { cache: 'force-cache', // Explicitly enable full route cache // next: { revalidate: 60 } // Revalidate every 60 seconds (stale-while-revalidate) next: { tags: ['products'] } // Revalidate via a tag, more efficient than path-based, if applicable }); if (!res.ok) { throw new Error('Failed to fetch data'); } return res.json(); } export default async function ProductsPage() { const products = await getProducts(); return ( <div> <h1>Products</h1> <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </ul> </div> ); } // In another location (e.g., form submission route): // import { revalidateTag } from 'next/cache'; // revalidateTag('products'); """ **Example (Client Component - with SWR):** """jsx // app/components/UserProfile.jsx 'use client'; import useSWR from 'swr'; const fetcher = (...args) => fetch(...args).then((res) => res.json()); export default function UserProfile({ userId }) { const { data, error } = useSWR("/api/user/${userId}", fetcher); if (error) return <div>Failed to load</div>; if (!data) return <div>Loading...</div>; return ( <div> <h1>{data.name}</h1> <p>{data.email}</p> </div> ); } """ **Anti-pattern:** Using "getServerSideProps" to fetch data that could be statically generated or cached. ### 1.2. Optimizing "fetch" Requests **Standard:** Optimize "fetch" requests for efficiency. * **Do This:** * Use "cache: 'force-cache'" for data that rarely changes to leverage the full route cache. * Use "next: { revalidate: <seconds> }" for data that changes periodically, implementing a stale-while-revalidate pattern. This provides a fast initial load with eventual consistency. Prefer "tags" over "revalidatePath" when possible. * Use "unstable_noStore()" if data must always be fresh and cannot be cached (e.g., user-specific data). But be aware of the performance implications. * Set appropriate "cache-control" headers on your external API endpoints when possible to influence browser caching behavior. * Consider using "transform: " options in fetch to minimize data transfer. * Batch multiple API requests into a single request (if the API allows) to reduce network overhead. * Use environment variables for API keys and sensitive information. * **Don't Do This:** * Make redundant API requests. Cache the results and reuse them where appropriate. * Fetch excessively large datasets when only a subset is needed. Paginate or filter data on the server. * Hardcode API keys directly into the codebase. **Why:** Efficient data fetching minimizes latency and reduces server load, resulting in a faster user experience. Caching strategies ensure that data is served quickly and efficiently when possible. **Example (Fetch with Revalidation):** """jsx // app/blog/[slug]/page.js async function getPost(slug) { const res = await fetch("https://api.example.com/posts/${slug}", { next: { revalidate: 3600 }, // Revalidate every hour }); if (!res.ok) { throw new Error('Failed to fetch post'); } return res.json(); } export default async function BlogPost({ params }) { const post = await getPost(params.slug); return ( <div> <h1>{post.title}</h1> <p>{post.content}</p> </div> ); } """ **Example (Fetch with Tags + Manual Revalidation):** """jsx // app/products/page.js async function getProducts() { const res = await fetch('https://api.example.com/products', { next: { tags: ['products'] } }); return res.json(); } // app/api/revalidate/route.js import { revalidateTag } from 'next/cache' import { NextResponse } from 'next/server' export async function POST(request) { const tag = request.nextUrl.searchParams.get('tag') revalidateTag(tag) return NextResponse.json({ revalidated: true, now: Date.now() }) } """ **Anti-pattern:** Refreshing cached data too frequently without good reason. This defeats the purpose of caching. ## 2. Image Optimization ### 2.1. Using the "next/image" Component **Standard:** Utilize the "next/image" component for optimal image delivery. * **Do This:** * Replace native "<img>" tags with "<Image>" from "next/image". * Specify "width" and "height" attributes to prevent layout shift. * Use the "priority" attribute for images critical to the initial viewport (e.g., hero images) to prioritize loading. * Configure "sizes" attribute for responsive images. * Use "loader" prop when using external image providers. * **Don't Do This:** * Use native "<img>" tags without optimization. * Omit "width" and "height" attributes, leading to CLS (Cumulative Layout Shift). * Serve excessively large images that are not optimized for the viewport. **Why:** The "next/image" component optimizes images by automatically resizing, optimizing, and serving them in modern formats like WebP. It also provides lazy loading, reducing initial page load time. Proper "width" and "height" attributes prevent layout shift during image loading, improving the user experience. **Example:** """jsx import Image from 'next/image'; function MyComponent() { return ( <Image src="/images/profile.jpg" // Route to your image file alt="Profile picture" width={500} // Required height={500} // Required priority // Prioritize loading for initial viewport images sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> ); } """ **Anti-pattern:** Using "next/image" without specifying dimensions. This defeats its main purpose of preventing layout shift. ### 2.2. Image Optimization Strategies **Standard:** Implement appropriate image optimization strategies. * **Do This:** * Use a Content Delivery Network (CDN) for serving images globally for faster delivery. Consider Next.js's built-in image optimization with Vercel. * Optimize images using tools like TinyPNG or ImageOptim before uploading. * Consider using placeholder blurs (blurDataURL) for a smoother loading experience. * Use responsive images with different sizes for different screen sizes. * **Don't Do This:** * Serve unoptimized images directly from your server. * Ignore image optimization due to perceived complexity. **Why:** Image optimization dramatically reduces the size of images, leading to faster load times and reduced bandwidth consumption. **Example (Blur Placeholder):** """jsx import Image from 'next/image'; function MyComponent() { return ( <Image src="/images/profile.jpg" alt="Profile picture" width={500} height={500} placeholder="blur" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAFoEvfhAAAADUlEQVR42mP88W8AARAFA6wZkAIAAAAASUVORK5CYII=" /> ); } """ **Anti-pattern:** Embedding extremely large images directly in the code without any optimization. ## 3. Code Splitting and Lazy Loading ### 3.1. Dynamic Imports **Standard:** Use dynamic imports to split code into smaller chunks that are loaded on demand. * **Do This:** * Use "next/dynamic" to lazy-load components that are not immediately needed. * Use dynamic imports for large libraries or components that are used infrequently. * Specify a "loading" component to display while the dynamic component is loading. * **Don't Do This:** * Import everything upfront, increasing the initial bundle size. * Neglect to provide a loading state for dynamically imported components. **Why:** Code splitting reduces the initial JavaScript bundle size, leading to faster initial page load times. Dynamic imports allow you to load components only when they are needed, further improving performance. **Example:** """jsx import dynamic from 'next/dynamic'; const DynamicComponent = dynamic(() => import('../components/MyComponent'), { loading: () => <p>Loading...</p>, }); function HomePage() { return ( <div> <p>This is the home page</p> <DynamicComponent /> </div> ); } export default HomePage; """ **Anti-pattern:** Importing large, unnecessary modules into every component. ### 3.2. Route-Based Code Splitting **Standard:** Leverage Next.js's automatic route-based code splitting. * **Do This:** * Organize your application into multiple pages and components. * Ensure that each route only loads the necessary code for that specific page. * Take advantage of Next.js's automatic code splitting to minimize the initial bundle size. * **Don't Do This:** * Create a single, monolithic page with all components included. * Load unnecessary code on routes where it is not needed. **Why:** Route-based code splitting allows Next.js to split your application into separate bundles for each route. This reduces the initial load time by only loading the code needed for the current page. **Anti-pattern:** Creating a single page application with everything included for all routes. ## 4. Memoization and useCallback ### 4.1. Using "React.memo" **Standard:** Use "React.memo" to prevent unnecessary re-renders of components. * **Do This:** * Wrap pure components with "React.memo" to prevent re-renders when props haven't changed. * Use "React.memo" in conjunction with "useCallback" to prevent re-creating function props. * **Don't Do This:** * Wrap every component with "React.memo" without considering the cost of prop comparisons. * Memoize components with frequently changing props. **Why:** "React.memo" optimizes performance by preventing unnecessary re-renders of components. This is especially useful for pure components that render the same output given the same props. **Example:** """jsx import React from 'react'; const MyComponent = React.memo(function MyComponent({ data }) { console.log('Rendering MyComponent'); // Check when it re-renders return ( <div> {data.name} </div> ); }); export default MyComponent; """ **Anti-pattern:** Overusing "React.memo" can be detrimental if prop comparisons are more expensive than re-rendering the component. ### 4.2. Using "useCallback" **Standard:** Use "useCallback" to memoize functions passed as props to child components. * **Do This:** * Wrap functions passed as props with "useCallback" to prevent them from being recreated on every render. * Provide the dependency array correctly to avoid stale closures. * Only use "useCallback" when passing functions to memoized components or when functions are used in effects. * **Don't Do This:** * Create new functions inline as props to child components. * Use "useCallback" without providing a dependency array. **Why:** "useCallback" ensures that the same function instance is passed as a prop to child components, preventing unnecessary re-renders. **Example:** """jsx import React, { useCallback } from 'react'; function ParentComponent({ onIncrement }) { const increment = useCallback(() => { onIncrement(); }, [onIncrement]); // Only recreate if onIncrement changes return ( <button onClick={increment}>Increment</button> ); } export default ParentComponent; """ **Anti-pattern:** Creating new anonymous functions directly as props to child components without using "useCallback". ## 5. Avoiding Large Components **Standard:** Break down large components into smaller, more manageable components. * **Do This:** * Identify distinct sections of a large component and extract them into separate components. * Create reusable components for common UI elements. * Use a component-driven architecture to improve maintainability and testability. * **Don't Do This:** * Create monolithic components with hundreds of lines of code. * Duplicate code across multiple components. **Why:** Smaller components are easier to understand, test, and maintain. They also improve performance by allowing React to efficiently update only the parts of the UI that have changed. ## 6. Minimizing Client-Side JavaScript **Standard:** Reduce the amount of JavaScript sent to the client. * **Do This:** * Move logic to the server using Server Actions where appropriate. They allow leveraging server-side capabilities while maintaining a good user experience. * Use Server Components more extensively to reduce the amount of client-side JavaScript. * Remove unused dependencies. * Use tools like webpack-bundle-analyzer to identify large bundles and optimize them. * Consider alternatives to large client-side libraries when possible. * **Don't Do This:** * Rely too heavily on client-side JavaScript for tasks that could be performed on the server. * Include unnecessary dependencies in your project. **Why:** Reducing client-side JavaScript improves initial page load time and reduces the amount of work the browser has to do, leading to a faster and more responsive user experience. **Example (Server Action):** """jsx // lib/actions.js 'use server' export async function addNote(prevState, formData) { // ... // revalidatePath('/notes'); } // app/notes/page.tsx import { addNote } from './lib/actions' export default function Page() { return ( <form action={addNote}> <input type="text" name="content" /> <button type="submit">Add Note</button> </form> ) } """ **Anti-pattern:** Performing heavy computations or data transformations on the client when they could be done on the server. ## 7. Performance Monitoring and Profiling **Standard:** Monitor and profile your Next.js application to identify and address performance bottlenecks. * **Do This:** * Use browser developer tools to measure page load time, rendering performance, and network activity. * Use the Next.js Profiler to identify slow components and rendering bottlenecks. * Monitor application performance in production using tools like Vercel Analytics, Google Analytics, or Sentry. * Set performance budgets and track them over time. * **Don't Do This:** * Ignore performance issues until they become critical. * Rely solely on anecdotal evidence to identify performance problems. **Why:** Performance monitoring and profiling provide valuable insights into your application's performance, allowing you to identify and address potential bottlenecks before they impact the user experience. Using tools is crucial for proactive optimization. ## 8. Font Optimization **Standard:** Optimize the loading and rendering of fonts. * **Do This:** * Use "next/font" to load fonts optimized for performance and privacy. It automatically inlines CSS and self-hosts font files. * Use "font-display: swap;" to ensure text is visible even before the font is loaded. * Preload important fonts using "<link rel="preload">". "next/font" handles this automatically. * Use variable fonts to reduce file size. * **Don't Do This:** * Load fonts from external URLs without optimization. * Block rendering while fonts are loading. * Use too many different font weights and styles. **Why:** Optimized fonts improve initial render times and prevent layout shift, leading to a better user experience. **Example ("next/font"):** """jsx import { Inter } from 'next/font/google' const inter = Inter({ subsets: ['latin'] }) export default function RootLayout({ children }) { return ( <html lang="en" className={inter.className}> <body>{children}</body> </html> ) } """ **Anti-pattern:** Loading fonts from external sources without any performance consideration. ## 9. Accessibility Considerations While primarily a performance optimization document this has implications for accessibility: * **Do This:** * Ensure images have descriptive "alt" text. This is critical not just for accessibility but for SEO benefits that contribute to overall site performance. * Use semantic HTML to improve performance by allowing browsers to better understand the structure of the page and improve rendering speed. * **Don't Do This:** * Use generic "alt" text or leave it blank. * Overuse JavaScript for core functionality. Semantic HTML allows assistive technologies to parse the content correctly. **Why:** While accessibility isn't directly a performance metric, ensuring accessibility often aligns with performance best practices as well. A well-structured, semantically correct page is generally faster to render and easier to navigate. ## 10. General Best Practices * Keep dependencies up to date. Regularly update your dependencies to benefit from performance improvements and bug fixes. * Lint your code. Use ESLint and Prettier to enforce consistent coding style and identify potential issues. * Test your code. Write unit and integration tests to ensure that your application is working correctly. * Profile before you optimize. Don't assume you know where the performance bottlenecks are. Use profiling tools to identify them first. By adhering to these performance optimization standards, developers can build fast, responsive, and efficient Next.js applications that provide a great user experience. This comprehensive document provides actionable guidance, code examples, and explanations that can be adopted by a professional development team.
# Testing Methodologies Standards for Next.js This document outlines the testing methodologies standards for Next.js applications, providing a comprehensive guide for developers to ensure high-quality, maintainable, and reliable code. It covers unit, integration, and end-to-end testing, highlighting best practices, common anti-patterns, and specific code examples tailored for Next.js projects. ## 1. Introduction to Testing in Next.js Testing is a critical aspect of software development, especially in web applications like those built with Next.js. Thorough testing ensures that your application behaves as expected, is resilient to changes, and provides a smooth user experience. This document provides practical guidelines for implementing effective testing strategies in Next.js projects. ### 1.1 Why Testing Matters in Next.js * **Reliability:** Ensures the application functions correctly under various scenarios. * **Maintainability:** Simplifies code refactoring and updates without introducing new bugs. * **Performance:** Identifies performance bottlenecks early in the development lifecycle. * **User Experience:** Guarantees a consistent and positive user experience. * **Early Bug Detection:** Finds and fixes issues before they reach production. ### 1.2 Testing Pyramid The testing pyramid is a helpful model for determining the types and amounts of testing to perform. It suggests focusing on unit tests, with fewer integration and end-to-end tests. * **Unit Tests:** Verify individual components or functions in isolation. * **Integration Tests:** Ensure that different parts of the application work together correctly. * **End-to-End (E2E) Tests:** Simulate user interactions to validate the entire application flow. ## 2. Unit Testing Unit tests focus on individual components, functions, or modules in isolation. In Next.js, this typically means testing React components, utility functions, and API routes individually. ### 2.1 Standards for Unit Testing * **Do This:** Write unit tests for all React components, utility functions, and API route handlers. * **Don't Do This:** Skip unit tests for "simple" components or functions; all code should be tested. * **Why:** Ensures each part of the application works correctly independently, simplifying debugging and maintenance. ### 2.2 Tools and Libraries * **Jest:** A popular JavaScript testing framework with built-in support for mocking and spies. * **React Testing Library:** Encourages testing components from a user's perspective, focusing on behavior rather than implementation details. * **msw (Mock Service Worker):** Enables mocking network requests directly in the browser or Node.js environment. ### 2.3 Code Examples #### 2.3.1 Testing React Components """jsx // components/Counter.jsx import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); const increment = () => { setCount(count + 1); }; const decrement = () => { setCount(count - 1); }; return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </div> ); } export default Counter; """ """jsx // __tests__/Counter.test.jsx import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import Counter from '../components/Counter'; describe('Counter Component', () => { it('should render the initial count', () => { render(<Counter />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); it('should increment the count when the increment button is clicked', () => { render(<Counter />); fireEvent.click(screen.getByText('Increment')); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); it('should decrement the count when the decrement button is clicked', () => { render(<Counter />); fireEvent.click(screen.getByText('Decrement')); expect(screen.getByText('Count: -1')).toBeInTheDocument(); }); }); """ * **Explanation:** This example demonstrates a simple React component and its corresponding unit tests using React Testing Library. The tests verify the initial state and the behavior of the increment and decrement buttons. #### 2.3.2 Testing Utility Functions """javascript // utils/formatCurrency.js export function formatCurrency(amount, currency = 'USD') { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency, }).format(amount); } """ """javascript // __tests__/formatCurrency.test.js import { formatCurrency } from '../utils/formatCurrency'; describe('formatCurrency', () => { it('should format a number as USD currency', () => { expect(formatCurrency(1234.56)).toBe('$1,234.56'); }); it('should format a number as EUR currency', () => { expect(formatCurrency(1234.56, 'EUR')).toBe('€1,234.56'); }); it('should handle zero amounts', () => { expect(formatCurrency(0)).toBe('$0.00'); }); }); """ * **Explanation:** This code shows unit tests for a utility function that formats numbers as currency. The tests cover different currencies and edge cases like zero amounts. #### 2.3.3 Testing API Route Handlers """javascript // pages/api/hello.js export default function handler(req, res) { res.status(200).json({ message: 'Hello, world!' }); } """ """javascript // __tests__/pages/api/hello.test.js import handler from '../../pages/api/hello'; import { createMocks } from 'node-mocks-http'; describe('/api/hello', () => { it('should return a greeting message', async () => { const { req, res } = createMocks({ method: 'GET', }); await handler(req, res); expect(res._getStatusCode()).toBe(200); expect(JSON.parse(res._getData())).toEqual({ message: 'Hello, world!' }); }); }); """ * **Explanation:** This example tests an API route handler in Next.js using "node-mocks-http" to create mock requests and responses. ### 2.4 Common Anti-Patterns * **Testing Implementation Details:** Focus on testing the output and behavior of components rather than their internal implementation. Use React Testing Library to simulate user interactions. * **Skipping Edge Cases:** Neglecting to test edge cases (e.g., empty inputs, error conditions) can lead to unexpected behavior in production. * **Over-mocking:** Excessive mocking can make tests brittle and less reflective of actual application behavior. Use mocks judiciously, especially when interacting with external APIs. ### 2.5 Modern Approaches * **Component-Driven Testing:** Tools like Storybook can be integrated to write tests that verify component behavior in isolation and across various states. * **Snapshot Testing (with caution):** Useful for ensuring UI consistency, but avoid relying on them solely due to their potential for false positives. Use snapshots alongside behavioral tests. * **Mocking with MSW:** "msw" allows mocking API requests directly within your tests, making them less reliant on external API availability and more performant. ## 3. Integration Testing Integration tests verify that different parts of the application work together correctly. In Next.js, this often involves testing the interaction between components, pages, API routes, and external services. ### 3.1 Standards for Integration Testing * **Do This:** Test the interaction between components, pages, and API routes. * **Don't Do This:** Overlap with unit tests or end-to-end tests; focus on the "glue" between modules. * **Why:** Ensures that different parts of the application work together as expected, catching integration issues early. ### 3.2 Tools and Libraries * **Jest:** Can be used for integration tests, particularly for testing API routes and data fetching. * **React Testing Library:** Suitable for testing component integration and interactions. * **msw (Mock Service Worker):** Useful for mocking API requests in integration tests. * **Cypress:** A more complete testing solution that can also be used for integration tests, in addition to end-to-end tests. ### 3.3 Code Examples #### 3.3.1 Testing Component Integration """jsx // components/ProductList.jsx import React from 'react'; import ProductItem from './ProductItem'; function ProductList({ products }) { return ( <ul> {products.map((product) => ( <ProductItem key={product.id} product={product} /> ))} </ul> ); } export default ProductList; """ """jsx // components/ProductItem.jsx import React from 'react'; function ProductItem({ product }) { return ( <li> {product.name} - ${product.price} </li> ); } export default ProductItem; """ """jsx // __tests__/ProductList.test.jsx import React from 'react'; import { render, screen } from '@testing-library/react'; import ProductList from '../components/ProductList'; const mockProducts = [ { id: 1, name: 'Product 1', price: 20 }, { id: 2, name: 'Product 2', price: 30 }, ]; describe('ProductList Component', () => { it('should render a list of products', () => { render(<ProductList products={mockProducts} />); expect(screen.getByText('Product 1 - $20')).toBeInTheDocument(); expect(screen.getByText('Product 2 - $30')).toBeInTheDocument(); }); }); """ * **Explanation:** This example tests the integration between "ProductList" and "ProductItem" components. It verifies that the "ProductList" component correctly renders a list of "ProductItem" components. #### 3.3.2 Testing API Route Integration """javascript // pages/api/products.js export default async function handler(req, res) { const products = [ { id: 1, name: 'Product 1', price: 20 }, { id: 2, name: 'Product 2', price: 30 }, ]; res.status(200).json(products); } """ """jsx // components/ProductsPage.jsx import React, { useState, useEffect } from 'react'; function ProductsPage() { const [products, setProducts] = useState([]); useEffect(() => { async function fetchProducts() { const response = await fetch('/api/products'); const data = await response.json(); setProducts(data); } fetchProducts(); }, []); return ( <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </ul> ); } export default ProductsPage; """ """jsx // __tests__/ProductsPage.test.jsx import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import ProductsPage from '../components/ProductsPage'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; const server = setupServer( rest.get('/api/products', (req, res, ctx) => { return res(ctx.json([ { id: 1, name: 'Product 1' }, { id: 2, name: 'Product 2' }, ])); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe('ProductsPage Component', () => { it('should fetch and render a list of products', async () => { render(<ProductsPage />); await waitFor(() => { expect(screen.getByText('Product 1')).toBeInTheDocument(); expect(screen.getByText('Product 2')).toBeInTheDocument(); }); }); }); """ * **Explanation:** This example integrates the "ProductsPage" component with the "/api/products" API route using "msw" to mock the API response. The test verifies that the component correctly fetches and renders the list of products. ### 3.4 Common Anti-Patterns * **Testing Through the UI Only:** Integration tests should sometimes directly test the interactions between modules behind the UI to ensure that the correct data is passed and processed. * **Ignoring Error Handling:** Ensure integration tests cover error scenarios, such as API request failures or invalid data. * **Over-Reliance on Mocks:** While necessary, excessive mocking in integration tests can lead to a false sense of security. Strive for realistic scenarios by minimizing mock usage. ### 3.5 Modern Approaches * **Contract Testing:** Define clear contracts between components and services and test against these contracts to ensure compatibility. * **Using Real Databases in Test Environments:** For more realistic integration tests, consider using a real database in a test environment (e.g., a Docker container). Tools like Testcontainers simplify this setup. * **Mocking with environment variables:** Mock endpoints based on environment variables to switch between real and mock implementations for testing purposes. ## 4. End-to-End (E2E) Testing End-to-end (E2E) tests simulate user interactions with the application to validate the entire application flow. In Next.js, this involves testing the application from the user's perspective, ensuring that all components and services work together seamlessly. ### 4.1 Standards for E2E Testing * **Do This:** Test critical user flows, such as authentication, form submissions, and data display. * **Don't Do This:** Attempt to test every single interaction; focus on the most important paths. * **Why:** Provides the highest level of confidence that the application works correctly from the user's perspective. ### 4.2 Tools and Libraries * **Cypress:** A popular end-to-end testing framework with a user-friendly interface and powerful assertions. * **Playwright:** Another powerful E2E testing framework developed by Microsoft, offering cross-browser support and advanced features. * **Puppeteer:** A Node library that provides a high-level API to control Chrome or Chromium programmatically. ### 4.3 Code Examples #### 4.3.1 Testing User Authentication """javascript // cypress/integration/auth.spec.js describe('Authentication Flow', () => { it('should successfully log in a user', () => { cy.visit('/login'); cy.get('input[name="email"]').type('test@example.com'); cy.get('input[name="password"]').type('password123'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/dashboard'); cy.contains('Welcome, test@example.com').should('be.visible'); }); it('should display an error message for invalid credentials', () => { cy.visit('/login'); cy.get('input[name="email"]').type('invalid@example.com'); cy.get('input[name="password"]').type('wrongpassword'); cy.get('button[type="submit"]').click(); cy.contains('Invalid credentials').should('be.visible'); }); }); """ * **Explanation:** This Cypress test suite covers the authentication flow, verifying both successful login and error handling for invalid credentials. #### 4.3.2 Testing Form Submissions """javascript // cypress/integration/contact.spec.js describe('Contact Form Submission', () => { it('should successfully submit the contact form', () => { cy.visit('/contact'); cy.get('input[name="name"]').type('John Doe'); cy.get('input[name="email"]').type('john.doe@example.com'); cy.get('textarea[name="message"]').type('This is a test message.'); cy.get('button[type="submit"]').click(); cy.contains('Thank you for your message!').should('be.visible'); }); it('should display an error message for missing fields', () => { cy.visit('/contact'); cy.get('button[type="submit"]').click(); cy.contains('Name is required').should('be.visible'); cy.contains('Email is required').should('be.visible'); cy.contains('Message is required').should('be.visible'); }); }); """ * **Explanation:** This Cypress test suite validates the contact form submission process, ensuring that the form can be submitted successfully and that error messages are displayed for missing fields. ### 4.4 Common Anti-Patterns * **Flaky Tests:** E2E tests can be prone to flakiness due to network issues, timing problems, or inconsistent test environments. Implement retry mechanisms and use explicit waits to mitigate flakiness. * **Poor Test Isolation:** Ensure each test is independent and does not rely on the state of previous tests. Use "beforeEach" and "afterEach" hooks to reset the application state between tests. * **Ignoring Accessibility:** Ensure your E2E tests also verify accessibility attributes and keyboard navigation to provide an inclusive user experience. ### 4.5 Modern Approaches * **Visual Regression Testing:** Use tools like Percy or Chromatic to detect visual changes in your application's UI. This helps prevent unintended UI regressions. * **Parallel Test Execution:** Run E2E tests in parallel to reduce test execution time. Tools like Cypress Cloud and Playwright support parallel test execution. * **Component Testing with Cypress or Playwright:** Both frameworks now support component testing, bridging the gap between unit and E2E tests and enabling more focused testing of individual components within a real browser environment. ## 5. Continuous Integration and Continuous Deployment (CI/CD) Integrating testing into your CI/CD pipeline ensures that tests are run automatically whenever code changes are made. This helps catch issues early and prevent broken code from being deployed to production. ### 5.1 Standards for CI/CD * **Do This:** Integrate unit, integration, and E2E tests into your CI/CD pipeline. * **Don't Do This:** Rely solely on manual testing before deployments; automate the testing process. * **Why:** Ensures consistent and reliable testing, reducing the risk of deploying broken code. ### 5.2 Tools and Platforms * **GitHub Actions:** A popular CI/CD platform integrated directly into GitHub. * **GitLab CI:** Another widely used CI/CD platform integrated into GitLab. * **CircleCI:** A cloud-based CI/CD platform with flexible configuration options. * **Jenkins:** An open-source automation server commonly used for CI/CD. ### 5.3 Code Examples (GitHub Actions) """yaml # .github/workflows/ci.yml name: CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Use Node.js 18 uses: actions/setup-node@v3 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Run unit tests run: npm run test:unit - name: Run integration tests run: npm run test:integration - name: Run end-to-end tests uses: cypress-io/github-action@v5 with: start: npm start wait-on: 'http://localhost:3000' """ * **Explanation:** This GitHub Actions workflow defines a CI pipeline that runs unit, integration, and end-to-end tests whenever code is pushed to the "main" branch or a pull request is created. It also caches the "npm" dependencies to speed up the build process. The Cypress action is configured to start the Next.js development server and wait for it to be ready before running the tests. ### 5.4 Common Anti-Patterns * **Skipping Tests in CI/CD:** Ensure that all tests are run in the CI/CD pipeline; never skip tests to speed up deployments. * **Insufficient Test Coverage:** Monitor test coverage metrics to identify areas of the application that are not adequately tested. * **Long-Running CI/CD Pipelines:** Optimize your CI/CD pipeline to reduce build and test execution time. Parallelize tests, use caching, and optimize build configurations. ### 5.5 Modern Approaches * **Environment-Specific Configurations:** Use environment variables to configure tests for different environments (e.g., development, staging, production). * **Test Reporting:** Integrate test reporting tools to track test results and identify trends. Tools like Jest and Cypress Cloud provide detailed test reports and analytics. * **Triggering E2E tests on Preview Deployments:** Use preview deployments (e.g., on Vercel or Netlify) to run E2E tests against the deployed application before merging changes to the main branch. By following these testing methodologies standards, Next.js developers can ensure the creation of robust, maintainable, and high-quality applications. The combination of unit, integration, and end-to-end testing, along with modern tools and CI/CD integration, will lead to more reliable software and a better user experience.