Lesson 04-Next.js Application Routing-Rendering

Server Components

Server components are React components that run on the server, with access to the server environment, making them ideal for data fetching.

// app/page.js
import { groq } from "next-sanity";
import client from "@/lib/sanity.client";

export default async function Page() {
  const query = groq`*[_type == "post"] {
    title,
    author->name,
    _id
  }`;

  const posts = await client.fetch(query);

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post._id}>
            <h2>{post.title}</h2>
            <p>By {post.author.name}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

Client Components

Client components are React components that run on the client, suitable for handling user interactions and state management.

// app/page.js
import { useState, useEffect } from "react";

export default function Page() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const res = await fetch("/api/data");
      if (!res.ok) {
        throw new Error("Failed to fetch data");
      }
      const data = await res.json();
      setData(data);
    }

    fetchData().catch(console.error);
  }, []);

  return (
    <div>
      <h1>Data</h1>
      {data ? (
        <pre>{JSON.stringify(data, null, 2)}</pre>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

Dynamic Routes

Dynamic routes allow fetching data based on parameters using generateStaticParams and generateDynamicParams.

// app/posts/[id]/page.js
import { groq } from "next-sanity";
import client from "@/lib/sanity.client";

export async function generateStaticParams() {
  const query = groq`*[_type == "post"]{_id}`;
  const posts = await client.fetch(query);
  return posts.map((post) => ({ id: post._id }));
}

export default async function PostPage({ params }) {
  const query = groq`*[_type == "post" && _id == $id][0] {
    title,
    content,
    author->name,
    _id
  }`;
  const post = await client.fetch(query, { id: params.id });

  return (
    <div>
      <h1>{post.title}</h1>
      <p>By {post.author.name}</p>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </div>
  );
}

Server Actions

Server actions enable asynchronous operations on the server, such as data fetching or submission.

// app/page.js
import { groq } from "next-sanity";
import client from "@/lib/sanity.client";

export default async function Page() {
  const posts = await getPosts();

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post._id}>
            <h2>{post.title}</h2>
            <p>By {post.author.name}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

async function getPosts() {
  const query = groq`*[_type == "post"] {
    title,
    author->name,
    _id
  }`;
  const posts = await client.fetch(query);
  return posts;
}

Server Rendering

Server rendering generates HTML content on the server and sends it to the client, which is the default behavior in Next.js.

// app/page.js
export default function Page() {
  return (
    <div>
      <h1>Welcome to My App</h1>
      <p>This is a server-rendered page.</p>
    </div>
  );
}

Client Rendering

Client rendering generates and updates the DOM on the client, typically used for interactive components.

// app/page.js
import { useState } from "react";

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

  return (
    <div>
      <h1>Welcome to My App</h1>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Incremental Static Generation (ISR)

Incremental Static Generation allows pre-generating static pages and updating them on-demand after deployment.

// app/posts/[id]/page.js
import { groq } from "next-sanity";
import client from "@/lib/sanity.client";

export async function generateStaticParams() {
  const query = groq`*[_type == "post"]{_id}`;
  const posts = await client.fetch(query);
  return posts.map((post) => ({ id: post._id }));
}

export async function getStaticProps({ params }) {
  const query = groq`*[_type == "post" && _id == $id][0] {
    title,
    content,
    author->name,
    _id
  }`;
  const post = await client.fetch(query, { id: params.id });

  return {
    props: {
      post,
    },
    revalidate: 60, // Revalidate every 60 seconds
  };
}

export default function PostPage({ post }) {
  return (
    <div>
      <h1>{post.title}</h1>
      <p>By {post.author.name}</p>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </div>
  );
}

Lazy Loading

Lazy loading is an optimization technique that loads components only when needed, improving the initial load speed of the application.

// app/page.js
import { lazy, Suspense } from "react";

const LazyComponent = lazy(() => import("./LazyComponent"));

export default function Page() {
  return (
    <div>
      <h1>Welcome to My App</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

Concurrent Rendering

Concurrent rendering, introduced in React 18, allows React to render components in parallel on background threads, improving rendering performance.

// app/page.js
import { useEffect, useState } from "react";

export default function Page() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const res = await fetch("/api/data");
      if (!res.ok) {
        throw new Error("Failed to fetch data");
      }
      const data = await res.json();
      setData(data);
    }

    fetchData().catch(console.error);
  }, []);

  return (
    <div>
      <h1>Data</h1>
      {data ? (
        <pre>{JSON.stringify(data, null, 2)}</pre>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

Error Boundaries

Error boundaries are a React mechanism for catching and handling JavaScript errors in the component tree.

// app/ErrorBoundary.js
import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary";

export default function ErrorBoundary({ children, FallbackComponent }) {
  return (
    <ReactErrorBoundary FallbackComponent={FallbackComponent}>
      {children}
    </ReactErrorBoundary>
  );
}

// app/FallbackComponent.js
export default function FallbackComponent({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

// app/page.js
import ErrorBoundary from "./ErrorBoundary";
import FallbackComponent from "./FallbackComponent";

export default function Page() {
  return (
    <ErrorBoundary FallbackComponent={FallbackComponent}>
      <div>
        <h1>Welcome to My App</h1>
        <p>This is a server-rendered page.</p>
      </div>
    </ErrorBoundary>
  );
}

Redirects

Redirects allow redirecting users from one URL to another.

// app/api/redirect/route.js
export async function GET(request) {
  return new Response(null, {
    status: 301,
    headers: {
      location: "/new-url",
    },
  });
}

// app/page.js
import Link from "next/link";

export default function Page() {
  return (
    <div>
      <h1>Welcome to My App</h1>
      <p>This is a server-rendered page.</p>
      <Link href="/api/redirect">Redirect Me</Link>
    </div>
  );
}

Streaming Rendering

Streaming rendering enables progressive rendering of components, rather than waiting for all content to be ready before rendering.

// app/page.js
import { useEffect, useState } from "react";

export default function Page() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const res = await fetch("/api/data");
      if (!res.ok) {
        throw new Error("Failed to fetch data");
      }
      const data = await res.json();
      setData(data);
    }

    fetchData().catch(console.error);
  }, []);

  return (
    <div>
      <h1>Data</h1>
      {data ? (
        <pre>{JSON.stringify(data, null, 2)}</pre>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

Preloading

Preloading is an optimization technique that loads resources in advance for quick access when needed.

// app/page.js
import Link from "next/link";

export default function Page() {
  return (
    <div>
      <h1>Welcome to My App</h1>
      <p>This is a server-rendered page.</p>
      <Link href="/about" prefetch>
        About Us
      </Link>
    </div>
  );
}

Server Push

Server push is an optimization technique that allows the server to proactively send resources to the client without explicit requests.

// app/page.js
import { useEffect, useState } from "react";

export default function Page() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const res = await fetch("/api/data");
      if (!res.ok) {
        throw new Error("Failed to fetch data");
      }
      const data = await res.json();
      setData(data);
    }

    fetchData().catch(console.error);
  }, []);

  return (
    <div>
      <h1>Data</h1>
      {data ? (
        <pre>{JSON.stringify(data, null, 2)}</pre>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

Server-Side Rendering (SSR)

Server-side rendering generates HTML content on the server and sends it to the client.

// app/page.js
export default function Page() {
  return (
    <div>
      <h1>Welcome to My App</h1>
      <p>This is a server-rendered page.</p>
    </div>
  );
}
Share your love