Volver

Next.js Roadmap

Next.js by Vercel - The React Framework

Initialization

npx create-next-app@latest app-name

Router

image.png


Page

export default function Page({
  params,
  searchParams
}: {
  params: Promise<{ slug: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
  return <h1>My Page</h1>;
}

Layout

image.png


Loading

loading.js file can create instant loading states built on Suspense. By default, this file is a Server Component - but can also be used as a Client Component through the "use client" directive.

export default function Loading() {
  // Or a custom loading skeleton component
  return <p>Loading...</p>;
}

Not Found

The not-found.js file is used to render UI when the notFound function is thrown within a route segment. Along with serving a custom UI, Next.js will return a 200 HTTP status code for streamed responses, and 404 for non-streamed responses.

import Link from "next/link";

export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
      <Link href="/">Return Home</Link>
    </div>
  );
}

Import Alias

In Next.js, you can set up aliases for directories to simplify import statements by configuring the paths property in the tsconfig.json or jsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@components/*": ["components/*"],
      "@utils/*": ["utils/*"],
      "@styles/*": ["styles/*"]
    }
  }
}
// Before
import Header from "../../components/Header";

// After
import Header from "@components/Header";

Server Side Rendering (SSR)

This is the default rendering method on Next.js, where pages are generated on the server for each request. This approach provides better SEO, faster First Contentful Paint (FCP), and improved performance for content-heavy applications.

React Servers Components

They combine the best of SSR and client-side interactivity. They allow components to run on the server, reducing the JavaScript bundle size sent to the client while maintaining rich interactivity where needed. Key benefits include:

// Server Component
async function ServerComponent() {
  const data = await db.query("SELECT * FROM users");
  return <UserList users={data} />;
}

Client Side Rendering (CSR)

Client Side Rendering (CSR) is a rendering method where the initial HTML is minimal, and JavaScript is used to render the page content in the browser. In Next.js, CSR is typically used for highly interactive components or when SEO is not a primary concern. While CSR can provide a more dynamic user experience, it may result in slower initial page loads compared to server-side rendering methods.

“use client”

//Client Component
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

Static Site Generation (SSG)

Force Static Rendering

export const dynamic = "force-static";

export default async function Page() {
  const res = await fetch("https://api.example.com/data");
  const data = await res.json();

  return <div>{data.message}</div>;
}

Incremental Static Regeneration (ISR)

export default async function Page() {
  // Data will be cached and revalidated every 60 seconds
  const data = await fetch("https://api.example.com/data", {
    next: {
      revalidate: 60
    }
  });

  return <main>{data.title}</main>;
}

Lets you wrap components that may need to wait for data to load. It provides a declarative way to handle loading states in your React applications. Key benefits include:

import { Suspense } from "react";

export default function Page() {
  return (
    <Suspense fallback={<Loading />}>
      <SlowComponent />
    </Suspense>
  );
}

Metadata

Next.js has a Metadata API that can be used to define your application metadata (e.g. meta and link tags inside your HTML head element) for improved SEO. There are two ways you can add metadata to your app:

Static Metadata

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Next.js App",
  description: "Example web app for my portfolio"
};

export default function Page() {}

Dynamic Metadata

import type { Metadata, ResolvingMetadata } from "next";

type Props = {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  // read route params
  const id = (await params).id;

  // fetch data
  const product = await fetch(`https://.../${id}`).then((res) => res.json());

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

  return {
    title: product.title,
    openGraph: {
      images: ["/some-specific-page-image.jpg", ...previousImages]
    }
  };
}

export default function Page({ params, searchParams }: Props) {}

next/image

The <Image /> component is an extension of the HTML <img /> element, optimized for Next.js applications. It provides automatic image optimization features including:

import Image from "next/image";

export default function Page() {
  return (
    <Image
      src="/profile.png"
      width={500}
      height={500}
      alt="Picture of the author"
    />
  );
}

next/font

import { Inter, Lora, Source_Sans_3 } from "next/font/google";
import localFont from "next/font/local";

// define your variable fonts
const inter = Inter();
const lora = Lora();

// define 2 weights of a non-variable font
const sourceCodePro400 = Source_Sans_3({ weight: "400" });
const sourceCodePro700 = Source_Sans_3({ weight: "700" });

// define a custom local font where GreatVibes-Regular.ttf is stored in the styles folder
const greatVibes = localFont({ src: "./GreatVibes-Regular.ttf" });

export { inter, lora, sourceCodePro400, sourceCodePro700, greatVibes };
import { inter, lora, sourceCodePro700, greatVibes } from "../styles/fonts";

export default function Page() {
  return (
    <div>
      <p className={inter.className}>Hello world using Inter font</p>
      <p style={lora.style}>Hello world using Lora font</p>
      <p className={sourceCodePro700.className}>
        Hello world using Source_Sans_3 font with weight 700
      </p>
      <p className={greatVibes.className}>My title in Great Vibes font</p>
    </div>
  );
}
{
  "compilerOptions": {
    "paths": {
      "@/fonts": ["./styles/fonts"]
    }
  }
}
import { greatVibes, sourceCodePro400 } from "@/fonts";

next/scripts

The <Script /> component is an extension of the HTML <script /> tag that optimizes when and how scripts are loaded and executed. It provides several key features for script optimization:

import Script from "next/script";

export default function Page() {
  return (
    <>
      <Script
        src="https://example.com/script.js"
        strategy="afterInteractive"
        onLoad={() => console.log("Script loaded")}
      />
    </>
  );
}

next/link

<Link> is a React component that extends the HTML <a> element to provide prefetching and client-side navigation between routes. It is the primary way to navigate between routes in Next.js.


next/cache

revalidatePath

Allows you to purge cached data on-demand for a specific path and its page or layout.

revalidatePath(path: string, type?: 'page' | 'layout'): void;
import { revalidatePath } from "next/cache";
revalidatePath("/blog/post-1");

next/navigation

redirect

redirect(path: string, type?: 'redirect' | 'push'): void;

usePathname

"use client";

import { usePathname } from "next/navigation";

export default function Navigation() {
  const pathname = usePathname();

  return (
    <nav>
      <Link href="/" className={pathname === "/" ? "active" : ""}>
        Home
      </Link>
    </nav>
  );
}

useRouter

This hook provides client-side routing capabilities by enabling programmatic navigation, access to router events and manipulation of URL parameters. It’s particularly useful when you need to handle navigation programmatically or respond to route changes in your components.

"use client";

import { useRouter } from "next/navigation";

export default function NavigationButtons() {
  const router = useRouter();

  return (
    <div>
      <button onClick={() => router.push("/dashboard")}>Go to Dashboard</button>
      <button onClick={() => router.back()}>Go Back</button>
    </div>
  );
}
"use client";

import { useRouter } from "next/navigation";

export default function LoginForm() {
  const router = useRouter();

  const handleLogin = async () => {
    // After successful login
    router.replace("/dashboard"); // User can't go back to login page
  };
}

useSearchParams

"use client";

import { useSearchParams } from "next/navigation";

export default function SearchBar() {
  const searchParams = useSearchParams();
  const search = searchParams.get("q");

  return <div>Search query: {search}</div>;
}

params

export default async function Page({
  params
}: {
  params: Promise<{ slug: string }>;
}) {
  const slug = (await params).slug;
}

image.png


searchParams

http://localhost:3000/search?page=2&query=example
export default async function Page({ searchParams }) {
  const currentPage = Number(searchParams?.page) || 1;
  const query = searchParams?.query || "";

  return (
    <div>
      <h1>Search Results</h1>
      <Table query={query} page={currentPage} />
    </div>
  );
}

Server Actions

export default function Page() {
  async function create() {
    // Server Action
    "use server";
    // Mutate data
  }
  return "...";
}
"use server";

export async function create() {}
"use client";

import { create } from "@/app/actions";

export function Button() {
  return <button onClick={() => create()}>Create</button>;
}
export default function Page() {
  async function createInvoice(formData: FormData) {
    "use server";

    const rawFormData = {
      customerId: formData.get("customerId"),
      amount: formData.get("amount"),
      status: formData.get("status")
    };

    sql`
      INSERT INTO invoices (customer_id, amount, status)
      VALUES (${rawFormData.customerId}, ${rawFormData.amount}, ${rawFormData.status});
    `;
    revalidatePath("/invoices"); //We need to revalidate the path as the data has changed
  }

  return <form action={createInvoice}>...</form>;
}

Prisma

npx i prisma -D
npx prisma init --datasource-provider sqlite
// prisma/schema.prisma
//...

model Note {
  id Int @id @default(autoincrement())
  title String
  content String?
  createadAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
npx prisma migrate dev --name init
// src/libs/prisma.ts
import { PrismaClient } from "@prisma/client";

declare global {
  // eslint-disable-next-line no-var
  var prisma: PrismaClient | undefined;
}

export const db = global.prisma || new PrismaClient();

if (process.env.NODE_ENV !== "production") global.prisma = db;
// app/api/notes/[id]/route.ts

import { db } from "@/libs/prisma";

export async function GET(request: Request, { params }: Params) {
  try {
    const note = await db.note.findFirst({
      where: { id: Number(params.id) },
    });
    //...
 }
npx prisma studio

next-auth

npm i next-auth
// .env

NEXTAUTH_URL = "http://localhost:3000";
NEXTAUTH_SECRET = "mysecret";
// app/api/auth/[...nextauth]/route.ts

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

export const authOptions = {
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: {
          label: "Email",
          type: "text",
          placeholder: "[email protected]"
        },
        password: {
          label: "Password",
          type: "password",
          placeholder: "******"
        }
      },
      async authorize(credentials) {
        //... Validate email and password then return the user or an error
      }
    })
  ],
  pages: {
    signIn: "/auth/signin"
  }
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
// app/auth/signin/page.tsx

"use client";
import { signIn } from "next-auth/react";

function SignInPage() {
  const onSubmit = handleSubmit(async (data) => {
    const res = await signIn("credentials", {
      redirect: false,
      email: data.email,
      password: data.password,
    });
    if (res?.ok) {
      router.push("/dashboard");
      router.refresh();
    } else {
      setError(res?.error);
    }
  });

  return (
    <div>
      <form onSubmit={onSubmit} >
//...
// src/middleware.ts

export { default } from "next-auth/middleware";

export const config = {
  matcher: ["/dashboard/:path*"]
};

UI Component Libraries


Internationalization


Markdown


Architectures

Monolith

/src
/app
/api
/page.tsx
/cart
/page.tsx
/components
/ProductList.js
/CartSummary.js
/db  
 /lib
/models
/hooks
/styles

Feature-Slice Design

/src
/features
/auth
/components
/api
/hooks
/models
/products
/components
/api
/hooks
/cart
/components
/api
/hooks
/shared
/ui
/lib
/api

Monorepo

/
/apps
/web
/mobile
/admin
/packages
/ui-components
/utils
/config
/tools
/scripts
/testing

Resources

https://www.youtube.com/watch?v=jMy4pVZMyLM&list=PLTHsJ1otlcc-Xfz5DyrQe7dC1YynTjSnn&index=15

https://www.youtube.com/watch?v=m6KESRxAdK4&list=PLTHsJ1otlcc-Xfz5DyrQe7dC1YynTjSnn&index=16

https://www.youtube.com/watch?v=_SPoSMmN3ZU&list=PLTHsJ1otlcc-Xfz5DyrQe7dC1YynTjSnn&index=39

https://www.youtube.com/watch?v=2eAstzL1u_s&list=PLTHsJ1otlcc-Xfz5DyrQe7dC1YynTjSnn&index=39&pp=gAQBiAQB

https://www.youtube.com/watch?v=5k7ZGhL3pI0&list=PLTHsJ1otlcc-Xfz5DyrQe7dC1YynTjSnn&index=40&pp=gAQBiAQB

https://www.youtube.com/watch?v=iZDK42F2cTc&list=PLTHsJ1otlcc-Xfz5DyrQe7dC1YynTjSnn&index=41&pp=gAQBiAQB

https://www.youtube.com/watch?v=etyCqA7DnnI&list=PLTHsJ1otlcc-Xfz5DyrQe7dC1YynTjSnn&index=45&pp=gAQBiAQB

https://www.youtube.com/watch?v=AdkNcFUsRQQ&list=PLTHsJ1otlcc-Xfz5DyrQe7dC1YynTjSnn