DEV Community

Cover image for Breaking Down Next.js 14
Focus Reactive for FocusReactive

Posted on with Eugene Boruhov • Edited on • Originally published at focusreactive.com

Breaking Down Next.js 14

Next.js has established itself as a cornerstone in the React ecosystem, providing developers with a powerful and flexible framework for building server-rendered React applications. With each iteration, Next.js aims to push the boundaries of web development, focusing on performance enhancements.

The release of Next.js 14 marks another significant milestone in this journey. These updates come packed with a host of new features and optimizations designed to make building scalable, performant web applications more intuitive and efficient than ever before. From advanced routing capabilities to server components by default, Next.js continues to offer solutions that address the complex challenges of web development.

A highlight of these releases was shared during the 2023 conference, where the Next.js team unveiled the exciting new directions and enhancements that developers can look forward to. For a comprehensive recap of the Next.js 13/14 announcement and insights into the conference highlights, you can explore further details through this link.

As we delve into the specifics of these updates, it's clear that Next.js is not just keeping pace with the fast-moving web development landscape but is also setting new standards for what developers can achieve with React. The following sections will explore key features introduced in Next.js 14, offering explanations, benefits, and TypeScript code examples to illustrate the power and flexibility of these enhancements.

Note: all code examples in this article are written in TypeScript (.ts or .tsx), but you can use JavaScript (.js or .jsx) files for every example presented.

Table of contents

Routing

The routing system in Next.js has always been a standout feature, offering a simple yet powerful way to handle page navigation and URL management. With the release of Next.js, the framework introduces a redesigned more intuitive mechanism. It is based on specialized files and directory naming conventions. This new system is designed to provide developers with increased flexibility, better organization, and more control over the routing behavior of their applications.

The latest routing system introduces the concept of nested routing, allowing developers to structure their applications in a hierarchical manner that reflects the UI and navigational flow. This system is based on directories and files, where the arrangement of files within directories dictates the routing structure. Here's a breakdown of the specialized files and what they accomplish:

  • page.tsx defines a page component. It's the core of the routing system, where each page.tsx file corresponds to a route
  • layout.ts specifies a layout component that wraps around the page component, useful for defining common UI elements like headers and footers across different pages
  • template.tsx similar to layout.tsx, but doesn’t persist state between navigations
  • loading.tsx defines a loading component displayed during data fetching or when navigating between pages
  • error.tsx custom error component for handling errors within the application
  • not-found.tsx specifically for 404 pages, allowing developers to create custom "Page Not Found" responses
  • middleware.ts for defining middleware that can run before rendering a page, useful for authentication, redirects, and more
  • route.ts manages custom request handling for routes, including fetching and mutations
  • default.tsx a fallback component for when no specific page or layout is matched, ensuring a graceful handling of unexpected navigation scenarios
  • instrumentation.ts used for monitoring and measuring performance metrics or other custom logging for a route

Additionally, the directory names now support special syntax for dynamic, parallel, and intercepting routes, providing a robust solution for complex routing needs:

Route Groups: defined by directories enclosed in parentheses (...), it allows developers to apply a common set of layouts or data fetching logic to all routes within the group

  • Dynamic Routes: enclosed in brackets [...], allowing for variable paths and parameters
  • Parallel Routes: enabled by creating a directory with a parallel syntax @..., useful for loading multiple routes simultaneously
  • Intercepting Routes: achieved by creating directories enclosed in parentheses (..) to load a route from another part of your application within the current layout

Pages and Layouts

Pages and Layouts form the backbone of the Next.js routing system. The page.tsx file represents the entry point for a route, rendering the main content. On the other hand, layout.tsx and template.tsx provide a way to wrap this content within common UI elements or templates.

page.tsx and layout.tsx are essential for any Next.js project, acting as the main files that determine the structure and behavior of pages and their surrounding layouts. With the introduction of a new caching system in Next.js 14, data fetching has become more efficient, eliminating the concern of request waterfalls. fetch() calls are now cached, allowing for data requests at both the page and layout levels without duplicating network requests.

Page component

page.tsx is the heart of any page within a Next.js application. It's where the rendering logic begins, and components come together to form the page's content. Unlike previous versions, where data fetching might have required useEffect and useState hooks for asynchronous operations, Next.js 14 simplifies this process. Below is an example of a page Server Component that fetches data directly within the component's body. As a result, all data is fetched on the server side and cached.

// app/page.tsx

export default function RootPage() {
  const data = await fetch('https://api.example.com/data').then((response) => response.json())

  return (
    <div>
      <h1>Root Page</h1>
      <p>{data.message}</p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Difference between layout.tsx and template.tsx

layout.tsx and template.tsx offering developers control over how pages and their content are presented and behave across different routes.

  • Layouts are designed to persist across multiple routes within your application. They wrap around the content of pages, providing a consistent structure and look, such as shared headers, footers, and navigation menus. Importantly, layouts maintain their state and are not re-mounted when navigating between pages that use the same layout. This persistence is beneficial for maintaining user experience continuity, such as keeping sidebar states or animations consistent across page transitions.
  • Templates, while similar to layouts in that they also wrap around child layouts or pages, behave differently upon navigation. Templates create a new instance for each child on navigation, meaning that DOM elements are recreated, component state is reset, and effects are re-synchronized each time a user navigates to a route that uses the same template. This behavior is particularly useful when you want a fresh start for components shared across routes, without carrying over any previous state or effects.

Error Handling

In Next.js 14, handling errors has become more intuitive and granular with the introduction of the error.tsx file, functioning as an React's Error Boundary mechanism. This file can be placed at various levels within your application, allowing developers to manage error handling with fine-grained control. The error component must be a Client Component, enabling client-side error handling and providing a user-friendly feedback mechanism for when things go wrong.

The error.tsx component serves as a centralized place to catch and handle errors that occur during rendering or in other parts of your application. By strategically placing error.tsx files in different directories, you can tailor the error handling experience to specific parts of your application, ensuring that users are presented with appropriate messages and actions depending on where an error occurs.

Here's an example of an error.tsx file, illustrating how to define an error component. This component captures errors, logs them for diagnostic purposes, and offers a way to attempt recovery by re-rendering the affected component or segment.

// app/error.tsx

'use client' // Error components must be Client Component

import { useEffect } from 'react'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error)
  }, [error])

  return (
    <div>
      <h2>Something went wrong!</h2>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

Loading UI

loading.tsx acts as a Suspense wrapper, a concept familiar to those versed in React's asynchronous component handling. However, Next.js abstracts away the complexity typically associated with implementing Suspense and lazy loading, offering developers a convenient interface to display loading UIs efficiently.

The primary goal of using a loading.tsx file is to improve the perceived performance of web applications by minimizing content layout shifts and providing immediate visual feedback to users during loading states. This is particularly useful when implementing skeleton screens — UI placeholders that mimic the size and position of the actual content yet to be loaded. Skeleton screens help maintain the user's attention and minimize the jarring effect of content popping into view, creating a smoother and more engaging user experience.

Here's a simple example of how to implement a loading.tsx file in your Next.js application. This example demonstrates the use of a skeleton screen as part of the loading UI:

// app/loading.tsx

export default function Loading() {
  // You can add any UI inside Loading, including a Skeleton
  return <LoadingSkeleton />
}
Enter fullscreen mode Exit fullscreen mode

In this loading.tsx file, you can define any UI components that you'd like to display while your application's data is being fetched or components are being loaded. The LoadingSkeleton component referenced in the example would be a custom component designed to resemble the layout of the content that will eventually be loaded. It can be as simple or complex as necessary to match the layout of the actual content closely.

Route Groups

Route Groups introduce a method for grouping routes under a shared configuration, such as layouts or data fetching needs. This is accomplished by enclosing directories in braces (...), creating a logical grouping of routes that share common attributes or behaviors. This organization simplifies the application's structure by applying a single set of configurations to all routes within the group, enhancing maintainability and consistency across similar routes.

Imagine a segment /user within your Next.js application, structured under the /app/user directory. This segment includes three child segments: /user/settings, /user/me, and /user/auth.

/app
  /user
    /auth
      page.tsx           // Authentication page, with a unique layout
            layout.tsx
    /(profile)
            /settings
          page.tsx       // User settings page, shares layout with `/user/me`
            /me
          page.tsx       // User profile page, shares layout with `/user/settings`
      layout.tsx         // Shared layout for the (profile) group
Enter fullscreen mode Exit fullscreen mode

The first two segments, /user/settings and /user/me, share a common layout, signifying their related functionalities and design. However, /user/auth requires a distinct layout due to its different design and purpose.

To efficiently manage this, you can group the /user/settings and /user/me segments under a common Route Group named (profile), allowing them to share the same layout, while /user/auth remains outside this group, thus not inheriting the group's shared layout.

Parallel Routes

Parallel Routes enable the simultaneous loading of components, which is particularly useful for applications requiring multiple components to be rendered independently but within the same page layout. By creating a directory with a parallel syntax, developers can structure their applications to load components in parallel. This feature is particularly useful for complex applications that require displaying several segments of content simultaneously, such as admin dashboards with multiple panels or sections.

Imagine an admin section of a web application with two distinct pages: /admin/dashboard and /admin/logs. Instead of navigating between these two pages, you want them displayed together under a single page for convenience. Next.js facilitates this through the use of Parallel Routes, which are indicated by the @ sign in the directory names of the pages you want to render together.

To implement this, you would structure your directories and files like so:

/app
  /admin
    /@dashboard
      page.tsx  // Dashboard page content
    /@logs
      page.tsx  // Logs page content
    layout.tsx  // Common layout for the `admin` section
Enter fullscreen mode Exit fullscreen mode

The layout.tsx inside /app/admin will serve as a common layout for both the /admin/dashboard and /admin/logs segments.

Here's how you could implement layout.tsx to accommodate both Parallel Routes:

// app/admin/layout.tsx

export default function Layout({
  children,
  dashboard,
  logs,
}: {
  children: React.ReactNode
  dashboard: React.ReactNode
  logs: React.ReactNode
}) {
  return (
    <>
      <div className="admin-header">Admin Section</div>`

      <div className="admin-content">
        {dashboard}
        {logs}
      </div>

      {children} // Implicit slot for additional content or pages
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Layout component is designed to receive dashboard and logs as props, which correspond to the content of the /admin/@dashboard/page.tsx and /admin/@logs/page.tsx files, respectively. These props are then rendered within the layout, allowing both the dashboard and logs content to be displayed simultaneously under the admin section.

Handling Unmatched Slots with default.ts

Additionally, you can define a default.tsx file within the /app/admin directory to serve as a fallback for unmatched slots during the initial load or full-page reload scenarios. This ensures that a meaningful default view is always presented to the user, enhancing the application's usability and navigation experience.

// app/admin/default.tsx

export default function Default() {
  return <div>Welcome to the Admin Section. Please select a panel.</div>
}
Enter fullscreen mode Exit fullscreen mode

Intercepting Routes

Intercepting Routes offers a sophisticated mechanism for managing route transitions within an application, particularly when aiming to maintain the current layout or context. This feature enables the dynamic loading of content from different parts of the application into the current view, without necessitating a full context switch for the user. It's especially useful in scenarios where you want to overlay additional information or functionality on the existing page content, such as displaying detailed information in a modal or overlay.

Intercepting Routes operates on the concept of soft and hard navigation:

  • Soft Navigation: When a user interacts with elements within the application (e.g., clicking on a photo within a feed), Next.js can intercept the route (e.g., /photo/123), mask the URL, and overlay the content (e.g., photo details) over the current layout (e.g., /feed) without reloading the page or losing context.
  • Hard Navigation: If the user navigates directly to a specific route (e.g., by clicking a shareable URL or refreshing the page), the expected behavior is to render the full page dedicated to that route (e.g., the entire photo page) instead of an overlay. In this scenario, route interception does not occur, ensuring that direct navigation and shareable links work as intended.

Intercepting Routes can be defined using the (..) convention, mirroring the concept of relative paths but applied to route segments rather than the filesystem. This allows for flexible route matching at various levels within the application's structure.

Imagine an application with a project management board where clicking on a task within a project overlays task details without leaving the project context. Here’s how you might structure and implement this with Intercepting Routes:

/app
  /projects
    /[projectId]
      /tasks
        [taskId].tsx  // Task details page
      (..)tasks
        [taskId].tsx  // Task details overlay component
    layout.tsx        // Shared layout for the projects section
Enter fullscreen mode Exit fullscreen mode

In this structure, navigating to /projects/123/tasks/456 in soft navigation (e.g., clicking on a task in the project view) would invoke the Intercepting Route (..)tasks/[taskId].tsx, displaying the task details as an overlay within the /projects/[projectId] context. The URL is masked to reflect the task's route, but the page layout remains consistent with the project view.

However, if a user directly navigates to /projects/123/tasks/456 (hard navigation), the full task details page at /projects/[projectId]/tasks/[taskId].tsx is rendered, dedicating the entire page to the task's content.

// /app/projects/layout.tsx

export default function ProjectLayout({ children }) {
  // Content of the current route
  return <div>{children}</div>
}
Enter fullscreen mode Exit fullscreen mode

This approach with Intercepting Routes enables the creation of rich, interactive user interfaces where detailed content can be dynamically loaded into the current context, enhancing the user experience by maintaining layout continuity and minimizing disruptions.

Dynamic Routes

Dynamic Routes allow for the creation of routes that adapt based on the data passed to them, such as user IDs or product names. Enclosed in brackets [...], dynamic routes enable variable paths and parameters, offering flexibility in how applications handle data-driven navigation and rendering.

To implement a user profile page with a dynamic user ID in the URL (e.g., /user/<id>), you would use dynamic segments. For the user ID, the directory and file structure in Next.js would look something like this:

/app
  /user
    /[id]
      page.tsx  // Dynamic route component for user profiles
Enter fullscreen mode Exit fullscreen mode

In this structure, [id] is the dynamic segment that will match any specific user ID, allowing for the creation of a unique path for each user profile.

Next, you would implement the logic to display the user profile within page.tsx. This involves fetching user data based on the dynamic id and rendering it:

// app/user/[id]/page.tsx

export default async function UserProfilePage({ params }: { params: { id: string } }) {
  const { id } = params
  const user = await fetch(`https://api.example.com/user/${id}`).then((response) => response.json())

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

From generateStaticPaths() to generateStaticParams()

Next.js has evolved the way dynamic routes are generated. The generateStaticParams() function is a newer approach that simplifies the process of defining parameters for dynamic routes.

// Return a list of `params` to populate the `[id]` dynamic segment
export async function generateStaticParams() {
  const users = await fetch('https://api.example.com/users').then((response) => response.json())

  return users.map((user) => ({
    slug: user.id,
  }))
}

// Multiple versions of this page will be statically generated
// using the `params` returned by `generateStaticParams`
export default function Page({ params }: { params: { id: string } }) {
  const { id } = params
  // ...
}
Enter fullscreen mode Exit fullscreen mode

From getStaticProps() to fetch()

Using fetch() directly within components or pages in Next.js represents a move towards more dynamic data fetching strategies. This shift grants developers granular control over caching, with the ability to manage cache behaviors explicitly in each fetch() call, enabling tailored data handling and optimized performance for more interactive and responsive web applications. You can learn more about this in Data Fetching section.

Route Handlers

Route Handlers in Next.js 14 introduce a powerful way to handle custom requests within your application, leveraging the Web Request and Response APIs for better control over routing behaviors. This feature allows for the creation of server-side logic directly associated with specific routes, akin to API Routes found in the pages directory of earlier Next.js versions. Route Handlers are defined within a route.tsfile.

For example, to add authentication functionality to a user page located at /app/user/page.tsx, you would create a POST handler in a route.tsx file under the same directory path:

/app
  /user
    page.tsx   // User page component
    route.tsx  // Route handler for user-related operations
Enter fullscreen mode Exit fullscreen mode

The route.tsx file within the /app/user directory can include a POST method handler to deal with user authentication. This handler can process login requests by verifying user credentials against a database or authentication service, and then respond appropriately.

// app/user/route.tsx

export async function POST(request: Request) {
  // Parse the request body to extract login credentials
  const { username, password } = await request.json()

  // Example authentication logic (simplified for demonstration)
  const isAuthenticated = await authenticateUser(username, password) // Assume this function checks credentials

  if (isAuthenticated) {
    // Handle successful authentication
    // For example, set cookies, return user data, etc.
    return new Response(
      JSON.stringify({ success: true, message: 'User authenticated successfully' }),
      {
        status: 200,
        headers: {
          'Content-Type': 'application/json',
          // Set any relevant security headers or cookies here
        },
      },
    )
  } else {
    // Handle failed authentication
    return new Response(JSON.stringify({ success: false, message: 'Authentication failed' }), {
      status: 401,
      headers: {
        'Content-Type': 'application/json',
      },
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the POST handler within route.tsx processes login requests by checking the provided credentials. Upon successful authentication, it responds with a success message (and potentially sets cookies or tokens for session management). If authentication fails, it returns an appropriate error response.

Middleware

Middleware is a powerful feature for intercepting requests and applying custom logic before the request is processed further. It's particularly useful for implementing global functionalities such as authentication, redirects, and logging across your application.

In this example, we'll implement middleware to perform an authentication check on API routes within a specific part of the application. The middleware will ensure that only authenticated requests can access the API endpoints under /api/. If a request is not authenticated, the middleware will respond with an error message, preventing unauthorized access.

To implement middleware across your application while targeting specific routes, you place a middleware.ts file in the root app directory and then specify a matcher in the config object. This allows for precise control over which routes the middleware applies to. Here’s how to set it up:

// app/middleware.ts

import { NextRequest, NextResponse } from 'next/server'
import { isAuthenticated } from 'lib/auth'

// Limit the middleware to paths starting with `/api/`
export const config = {
  matcher: '/api/:function*',
}

export function middleware(request: NextRequest) {
  // Call our authentication function to check the request
  if (!isAuthenticated(request)) {
    // Respond with JSON indicating an error message
    return new Response(JSON.stringify({ success: false, message: 'Authentication failed' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    })
  }

  // If authenticated, proceed with the request
  return NextResponse.next()
}
Enter fullscreen mode Exit fullscreen mode

Redirecting

Redirecting is a crucial aspect of web development, allowing developers to guide users through the application flow or direct them away from deprecated pages. Next.js simplifies the process of implementing redirects, supporting both server-side and client-side redirection strategies. For a detailed guide on implementing redirects, consider exploring additional resources such as this comprehensive article on Next.js redirects.

Server Components

Next.js 14 introduce a paradigm shift towards Server Components, emphasizing a server-centric approach to component rendering. This significant update means that, by default, components in Next.js are now Server Components unless explicitly defined otherwise. This change is designed to optimize performance and enhance the developer experience by leveraging the server's capabilities for initial rendering, while still allowing for dynamic client-side interactions through Client Components.

The distinction between Server Components and Client Components provides developers with more control over rendering behavior and performance optimization. The introduction of the "use client" directive allows a clear and effective way to differentiate between components that should be rendered on the server versus those intended for the client.

Server Components

Server Components are rendered on the server and do not include any JavaScript that runs in the browser. This approach is optimal for delivering static content, accessing server-side resources, and performing tasks that do not require client-side interactivity. Since Server Components do not send JavaScript to the client, they help reduce the overall size of the page bundle, leading to faster load times.

Client Components

To designate a component as a Client Component, the "use client" directive is added at the top of a file, above all import statements. This directive serves as a boundary between Server and Client Component modules, ensuring that the specified component and any modules it imports, including child components, are bundled for client-side execution. This mechanism is crucial for components that rely on client-side interactivity, such as handling user inputs or executing JavaScript that interacts with the browser's API.

'use client'

import { useState } from 'react'

function InteractiveComponent() {
  // Component logic that requires the browser's JavaScript environment
  const [count, setCount] = useState(1)
  return (
    <div>
      Interactive content here: {count}
      <button onClick={() => setCount(count + 1)}></button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

In this example, the InteractiveComponent is explicitly marked to run on the client. This means it, along with all its dependencies, will be part of the client bundle. It allows for interactive elements, such as buttons or forms, to function as expected, leveraging the client's JavaScript capabilities.

Key Differences

  • Execution Environment: Server Components run on the server, while Client Components execute in the browser. This fundamental difference affects how and where the component's code is executed and the resources it can access.
  • Bundle Impact: Server Components contribute to the HTML output without adding to the JavaScript bundle sent to the browser, leading to lighter pages. In contrast, Client Components add to the JavaScript bundle, impacting the amount of code the client must download and execute.
  • Interactivity: Client Components are necessary for interactive parts of an application that require client-side JavaScript, such as event handlers or animations. Server Components are better suited for static content and tasks that can be completed server-side.

Data Fetching

Data fetching is a fundamental aspect of building interactive and dynamic web applications. In Next.js 14, the framework enhances the experience of fetching, caching, and revalidating data, aligning closely with React's architectural advancements. These improvements offer developers a variety of methods to retrieve data efficiently, whether it's on the server-side using the native fetch API or third-party libraries, or on the client-side through Route Handlers and client-based libraries. This versatility ensures that data can be fetched and managed in the most optimal way according to the application's architecture and user experience requirements.

Next.js extends the capabilities of the traditional fetch API, allowing for configurable caching and revalidation strategies that are tailored to the needs of server-rendered components. This integration not only simplifies the data fetching process within React's component model but also significantly improves performance by reducing unnecessary network requests and leveraging smart caching techniques. Whether fetching data for server components, utilizing Route Handlers for client-side requests, or implementing third-party libraries for complex data management.

Fetch Data on the Server using fetch()

In Next.js 14, fetching data on the server side has been aligned more closely with React's Server Components. This shift enhances the developer experience by simplifying how dynamic data is fetched and used within components, directly utilizing the native fetch() function without relying on specific Next.js data fetching methods like getServerSideProps or getStaticProps used in previous versions.

The fetch() function is utilized within Server Components to request data from APIs or other data sources during the server rendering process. This approach ensures that data fetching logic is colocated with the component that requires the data, promoting better data encapsulation and component reusability. fetch() calls made in Server Components are automatically optimized to run on the server, preventing sensitive data, such as API keys, from being exposed to the client.

// app/post/[id]/page.tsx

export async function Page({ params }: { params: { id: string } }) {
  const { id } = params
  const post = await fetch(`https://api.example.com/post/${id}`, { next: { revalidate: 3600 }).then((response) => response.json)

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Fetch Data on the Client using Route Handlers

In Next.js 14, fetching data on the client while maintaining server-side execution for data retrieval is efficiently handled through Route Handlers. This feature is especially useful for scenarios where sensitive information, such as API tokens, should not be exposed to the client-side environment. Route Handlers enable the execution of data fetching logic on the server, with the results seamlessly passed back to the client.

Route Handlers are server-side functions that can be called from client components. They execute on the server, fetching data or performing other server-side logic, and then return the data to the client component that initiated the call. This mechanism allows for secure data fetching patterns, where sensitive operations are kept on the server, away from the client's reach. You could find great example of Route Handler in the previous section.

Server Actions and Mutations

In addition to the improvements in data fetching capabilities, Next.js 14 introduce the concepts of Server Actions and Mutations. This feature represents a significant advancement in how data can be manipulated on the server side, simplifying the process of creating interactive applications that require data to be not only fetched but also updated, created, or deleted.

Server Actions and Mutations allow developers to define functions that can perform data modifications on the server side, directly from client-side events. These actions are defined in Server Components and can be invoked from Client Components, providing a seamless integration between the client and server. This approach leverages the strengths of the server-side environment, such as direct access to databases and secure execution of sensitive operations, while maintaining a fluid user experience on the client side.

Server Actions can be flexibly defined either directly within a Client Component or in a separate file, according to the needs of your application. Regardless of where the Server Action is defined, it is crucial to prefix the function or the entire file with the "use server" directive.

First, let's define a Server Action to handle the form submission. This action will be responsible for receiving form data and creating a new post. Place this action in a separate file to make it importable by Client Components.

// app/post/actions.ts

'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title')
  const content = formData.get('content')

  // Simulate creating a post by calling an API or interacting with a database
  const response = await fetch('https://api.example.com/post', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      // Include authentication headers as needed
    },
    body: JSON.stringify({ title, content }),
  })

  if (!response.ok) {
    throw new Error('Failed to create the post')
  }

  return response.json()
}
Enter fullscreen mode Exit fullscreen mode

Next, create a page component that includes a form for submitting a new post. This form will use the Server Action defined above to handle the submission.

// app/post/create/page.tsx

'use client'

import React from 'react'
import { createPost } from 'app/post/actions'

export default function CreatePostPage() {
  const handleSubmit = async (event) => {
    event.preventDefault()

    const formData = new FormData(event.target)
    try {
      const result = await createPost(formData)
      console.log('Post created successfully:', result)
      // Redirect or display a success message as needed
    } catch (error) {
      console.error('Failed to create the post:', error)
      // Handle errors or display an error message as needed
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Title
        <input name="title" type="text" required />
      </label>

      <label>
        Content
        <textarea name="content" required></textarea>
      </label>

      <button type="submit">Create Post</button>
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

In this example, the CreatePostPage component renders a simple form for creating a new post, with fields for the post's title and content. The form's onSubmit handler calls the createPost Server Action with the form data. Upon successful submission, you can redirect the user to another page or display a success message. In case of failure, you might want to show an error message to the user.

Caching

New Nex.js introduce several new caching strategies, enabling applications to serve dynamic content more efficiently while reducing load times and server pressure. Let's delve into the key aspects of these caching enhancements.

Request Memoization

Request memoization is a caching strategy that temporarily stores the results of HTTP requests, so subsequent requests for the same data can be served instantly from the cache rather than re-fetching from the server or external APIs. This technique is particularly effective for data that doesn't change frequently, allowing for faster response times and reduced server load.

Data Cache

The data cache feature in Next.js allows developers to cache the results of data fetching operations at the application level. This means that once data is fetched from a database or API, it can be stored in the cache and reused across multiple requests or components. This approach not only speeds up data retrieval but also minimizes redundant data fetching operations, contributing to overall application efficiency.

Full Route Cache

Full route caching enables the caching of entire pages or routes, including their HTML, CSS, and JavaScript content. This strategy is ideal for static pages or pages with content that changes infrequently. By serving pages directly from the cache, applications can achieve near-instantaneous load times, significantly improving the user experience, especially for sites with high traffic volumes.

Router Cache

Router cache refers to the caching of routing information and component states within the Next.js router. This caching strategy ensures that navigating back to previously visited pages is instant, as the framework can quickly restore the page from the cache without re-rendering the components or re-fetching data. This feature enhances the navigational speed and fluidity of Next.js applications, making them feel more responsive and app-like.

Metadata

Enhancing SEO and ensuring web applications are easily discoverable has always been a crucial part of web development. Next.js 14 introduce comprehensive improvements and new features around metadata management, making it simpler for developers to optimize their applications for search engines and social media platforms.

Static metadata object

In Next.js 14, developers can utilize a static metadata object to define page-specific metadata. This approach allows for the centralized management of SEO-related information, such as page titles, descriptions, and keywords. The metadata object is automatically parsed by Next.js and the relevant HTML meta tags are generated and included in the page header, ensuring that search engines and social media platforms can accurately interpret and display information about the page.

// app/page.tsx or app/layout.tsx

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: '...',
  description: '...',
}

export default function Page() {}
Enter fullscreen mode Exit fullscreen mode

generateMetadata function

For more dynamic scenarios, where metadata needs to be generated based on the page content or external data, Next.js introduces the generateMetadata function. This function can be exported from a page component and is called during the server-side rendering process. It receives the page props as an argument, allowing developers to dynamically generate metadata based on the page's content or other dynamic data sources.

import { Metadata, ResolvingMetadata } from 'next'

export async function generateMetadata({
  params,
  searchParams,
  parent,
}: {
  params: { id: string }
  searchParams: { [key: string]: string | string[] | undefined }
  parent: ResolvingMetadata
}): Promise<Metadata> {
  const { id } = params

  const product = await fetch(`https://api.example.com/product/${id}`).then((response) =>
    response.json(),
  )

  // optionally access and extend (rather than replace) parent metadata
  const parentImages = (await parent).openGraph?.images || []

  return {
    title: product.title,
    openGraph: {
      images: [`/product-${product.id}.jpg`, ...parentImages],
    },
  }
}

export default function Page() {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

File-based metadata

Next.js also supports file-based metadata, enabling developers to define metadata at the application level or for specific routes using dedicated files. This feature is particularly useful for setting global metadata values, such as the site name, default language, or social media images, which can be overridden on a page-by-page basis as needed.

  • Favicon and App Icons: Developers can place favicon.ico, apple-icon.jpg, and icon.jpg files in the public directory or a specific metadata directory to automatically generate the appropriate <link> tags for favicons and application icons.
  • Open Graph and Twitter Images: Similarly, placing opengraph-image.jpg and twitter-image.jpg files in the metadata directory allows Next.js to automatically generate Open Graph and Twitter card images, enhancing the appearance of shared links on social media platforms.
  • robots.txt: By creating a robots.txt file in the root directory, developers can control how search engines crawl and index the pages of their application, which is essential for SEO optimization. You can also dynamically generate this file just changing its name to robots.ts.
  • sitemap.xml: The same way like with robots.txt you have a choice, either to make this file yourself according to standard, or generate it dynamically with robots.ts file.
// app/robots.ts

import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: '/private/',
    },
    sitemap: 'https://example.com/sitemap.xml',
  }
}
Enter fullscreen mode Exit fullscreen mode
// app/sitemap.ts

import { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://example.com',
      lastModified: new Date(),
      changeFrequency: 'yearly',
      priority: 1,
    },
    {
      url: 'https://example.com/about',
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.8,
    },
    {
      url: 'https://exmaple.com/contacts',
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 0.5,
    },
  ]
}
Enter fullscreen mode Exit fullscreen mode

Dynamic Image Generation

Next.js 14 introduces a groundbreaking feature for dynamically generating images using the ImageResponse constructor. This innovative capability allows developers to craft images on-the-fly using JSX and CSS, perfect for creating customized social media images, including Open Graph images and Twitter cards. By leveraging the Edge Runtime, ImageResponse not only facilitates the creation of these dynamic images but also ensures that they are efficiently cached at the edge. This results in improved performance and reduced need for recomputation, as Next.js automatically manages the appropriate caching headers.

The process is simple and integrates with existing Next.js APIs, such as Route Handlers and file-based metadata, allowing for a wide range of applications from generating images at build time to serving them dynamically upon request. With support for common CSS properties, custom fonts, and even nested images.

Here's an example showcasing the Route Handler function alongside dynamically generated image:

import { ImageResponse } from 'next/server'

export const runtime = 'edge'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const id = searchParams.get('id')

  if (!id) return new Response('Missing id', { status: 400 })

  let user

  try {
    user = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then((res) => res.json())
  } catch (error) {
    console.error(error)
    return new Response('Error', { status: 500 })
  }

  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          textAlign: 'center',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        <span style={{ fontFamily: 'Righteous', color: stringToColour(user.name) }}>
          {user.name}
        </span>
        <span style={{ position: 'absolute', top: 0, left: 20 }}>🤯</span>
        <span style={{ position: 'absolute', top: 0, right: 20 }}>🥳</span>
        <span style={{ position: 'absolute', bottom: 10, left: 10, fontSize: 14 }}>{id}</span>
        <svg
          style={{ position: 'absolute', bottom: 20, right: 20 }}
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 170 61"
          width="170px"
          height="61px"
        >
          <path
            fillRule="evenodd"
            fill="black"
            d="M14.959 59.996L7.05 50.543H5.742v9.461H.001V38.907l5.14-6.566h6.347c2.227 0 4.252.658 5.858 1.926 1.582 1.247 2.702 3.037 3.155 5.32.34 1.718.238 3.492-.323 5.095-.557 1.591-1.546 3.017-2.982 4.076l-.003-.005a8.71 8.71 0 0 1-2.943 1.427l8.213 9.816h-7.504zm45.979 0l-6.535-18.254-6.684 18.254h-6.111l10.129-27.668 5.383.024 9.897 27.644h-6.079zm-21.465 0H24.161l-.02-20.914 5.141-6.742h10.735v5.818H32.08l-2.218 2.907.002 2.451h9.609v5.818h-9.604l.005 4.843h9.599v5.819zm111.681-27.647l-9.871 27.652h-5.382l-9.838-27.652h6.075l6.458 18.153 6.48-18.153h6.078zm-51.46 27.645v-21.85h-8.686V32.34h23.113v5.804h-8.685v21.85h-5.742zm17.681-.002V32.341h5.742v27.651h-5.742zm52.071.007h-17.694l-.02-20.92 5.167-6.739h13.095v5.788h-10.298l-2.243 2.926.003 2.478h11.99v5.788h-11.985l.005 4.89h11.98v5.789zm-53.557-54.02h-8.094c-1.497 0-4.194.753-4.175 2.614.025 2.467 2.661 2.489 4.572 2.682 1.494.152 3.272.348 4.94 1.019 2.113.851 3.85 2.333 4.444 5.027.698 3.158-.312 6.088-2.82 8.143-1.909 1.565-4.501 2.381-6.896 2.381h-8.094v-5.784h8.094c1.448 0 3.887-.51 4.161-2.411.107-.741-.113-1.637-1.019-2.001-.944-.38-2.259-.523-3.371-.634-3.527-.358-7.451-1.134-9.061-4.766-.771-1.737-.886-3.988-.339-5.808 1.245-4.133 5.505-6.245 9.564-6.245h8.094v5.783zM79.119.19l.001 18.75c0 .857.388 1.525.97 1.998.917.745 2.26 1.12 3.565 1.12v.01l5.681-.001V.19h5.741v27.656l-11.422.004v.011c-2.496 0-5.177-.805-7.153-2.411-1.872-1.522-3.123-3.693-3.123-6.51V.19h5.74zM0 27.846V6.892L5.273.19h10.962v5.811H8.026L5.741 8.905v2.735h9.24v5.805h-9.24v10.401H0zM37.817 8.272a7.982 7.982 0 0 0-5.695-2.385 8.04 8.04 0 0 0-1.92.222 7.61 7.61 0 0 0-1.275.44l-4.734 6.01a8.414 8.414 0 0 0-.125 1.47 8.16 8.16 0 0 0 2.359 5.757 7.99 7.99 0 0 0 5.695 2.384 7.99 7.99 0 0 0 5.695-2.384 8.158 8.158 0 0 0 2.359-5.757 8.155 8.155 0 0 0-2.359-5.757zM32.122-.015c3.836 0 7.309 1.573 9.822 4.114a14.076 14.076 0 0 1 4.07 9.93c0 3.877-1.556 7.388-4.07 9.929-2.513 2.541-5.986 4.114-9.822 4.114-3.835 0-7.309-1.573-9.822-4.114-3.873-3.915-4.78-8.865-3.462-14.055l6.344-8.054c1.226-.645 2.289-1.121 3.652-1.457a13.726 13.726 0 0 1 3.288-.407zM70.11 27.844c-2.726 0-5.548.058-8.249-.005-3.646-.086-6.94-1.617-9.343-4.047a13.856 13.856 0 0 1-4.007-9.776c0-3.818 1.531-7.275 4.007-9.779C54.993 1.736 58.412.188 62.189.188h7.921v5.804h-7.921a7.875 7.875 0 0 0-5.613 2.349 8.05 8.05 0 0 0-2.324 5.675 8.05 8.05 0 0 0 2.324 5.674 7.917 7.917 0 0 0 4.105 2.205c1.161.224 2.534.156 3.71.155h5.718l.001 5.794zm17.644 32.153c-2.726 0-5.547.058-8.248-.006-3.647-.085-6.941-1.616-9.344-4.046a13.861 13.861 0 0 1-4.006-9.777c0-3.818 1.53-7.276 4.006-9.778 2.475-2.502 5.894-4.05 9.671-4.05h7.921v5.804h-7.921a7.864 7.864 0 0 0-5.612 2.35 8.04 8.04 0 0 0-2.324 5.674c0 2.215.888 4.222 2.324 5.674a7.895 7.895 0 0 0 4.104 2.204c1.161.225 2.535.157 3.711.157l5.718-.001v5.795zM5.742 44.703h5.753c.509 0 .973-.062 1.379-.185.361-.112.677-.265.936-.46l.022-.022c.451-.333.764-.786.941-1.293a3.863 3.863 0 0 0 .121-2.016c-.164-.828-.535-1.45-1.047-1.853-.579-.457-1.395-.693-2.359-.693H7.894l-2.152 2.748v3.774z"
          ></path>
        </svg>
      </div>
    ),
    {
      width: 1200,
      height: 600,
      fonts: [
        {
          name: 'Righteous',
          data: await getFont({ origin }),
          weight: 400,
          style: 'normal',
        },
      ],
    },
  )
}

function getFont({ origin }: { origin: string }) {
  return fetch(`${origin}/fonts/Righteous-Regular.ttf`).then((res) => res.arrayBuffer())
}

const stringToColour = function (str: string) {
  let hash = 0
  for (let i = 0; i < str.length; i++) {
    hash = str.charCodeAt(i) + ((hash << 5) - hash)
  }
  let colour = '#'
  for (let i = 0; i < 3; i++) {
    let value = (hash >> (i * 8)) & 0xff
    colour += ('00' + value.toString(16)).substr(-2)
  }
  return colour
}
Enter fullscreen mode Exit fullscreen mode

Next.js 14 Dynamic Image Example

You can also experiment with it in your browser. Simply open this URL and try changing the id parameter to see different images.

Conclusion

As we wrap up our exploration of the dynamic capabilities of Next.js 14, it's clear that the framework continues to push the boundaries of modern web development. With its robust features for data fetching, dynamic routing, server actions, and more, Next.js offers developers an unparalleled toolkit for building highly interactive, performant web applications. Whether you're fetching data on the server or client, handling form submissions, or generating dynamic images, Next.js provides streamlined and efficient solutions that cater to a wide range of development needs.

This overview touches upon just a small selection of the new and enhanced features available in Next.js 14. To dive deeper into the full spectrum of capabilities and explore more advanced use cases, the comprehensive Next.js documentation serves as an invaluable resource.

At FocusReactive, our expertise in Next.js, Headless CMS, and eCommerce solutions positions us as a leading agency in navigating and leveraging these advanced features. Our commitment to staying at the forefront of technology enables us to deliver cutting-edge solutions that drive success for our clients' projects.

For further insights and best practices on leveraging Next.js and headless CMS technologies, we invite you to explore our additional resources. Whether you're deciding on the best headless CMS for Next.js, choosing a hosting platform, or optimizing images with Storyblok, our articles provide valuable guidance:

How to choose best hosting platform for you project:

For Storyblok users:

For Sanity users:

These resources are designed to help you navigate the complexities of modern web development, ensuring your projects are not only successful but also future-proof.

Top comments (0)