Technology · Next.js

Next.js Components

Build Next.js components using server and client rendering strategies for performance.

TL;DR
  1. 01Server components render on server, no JavaScript sent.
  2. 02Client components (use client) render in browser with state.
  3. 03Combine both for optimal performance and interactivity.

Server Components

  • Server components are the default in App Router.
    // app/components/BlogPost.tsx
    export default async function BlogPost({ slug }) {
      const post = await fetchPost(slug);
      return <article>{post.content}</article>;
    }
  • Can access databases, secrets, and APIs directly.
  • No JavaScript sent to browser for these components.
  • Cannot use hooks or browser APIs.
  • Use async/await directly in the component to fetch data.
    export default async function UserProfile({ id }) {
      const user = await db.user.findUnique({ where: { id } });
      return <p>{user.name}</p>;
    }
  • Pass props from server components to client components safely.
    export default async function Page() {
      const settings = await getSettings();
      return <ThemeProvider theme={settings.theme} />;
    }

Client Components

  • Mark components to render on client with "use client".
    "use client";
    
    import { useState } from "react";
    
    export default function Counter() {
      const [count, setCount] = useState(0);
      return <button onClick={() => setCount(count + 1)}>{count}</button>;
    }
  • Can use React hooks and browser APIs.
  • Must declare at top of file.
  • Use for interactive features.
  • Use useEffect to run code only after the component mounts.
    "use client";
    
    import { useEffect, useState } from "react";
    
    export function Clock() {
      const [time, setTime] = useState("");
      useEffect(() => {
        setTime(new Date().toLocaleTimeString());
      }, []);
      return <p>{time}</p>;
    }
  • Access browser APIs like localStorage inside client components.
    "use client";
    
    export function SaveButton({ data }) {
      function save() {
        localStorage.setItem("draft", JSON.stringify(data));
      }
      return <button onClick={save}>Save Draft</button>;
    }

Server and Client Composition

  • Pass server components as children to client components.
    "use client";
    
    export default function Layout({ children }) {
      return <div>{children}</div>;
    }
    
    // children can be server components
    <Layout>
      <ServerComponent />
    </Layout>
  • Keep sensitive data in server components, pass only safe props.
    // Server component fetches and sanitizes data
    export default async function Page() {
      const post = await getPost(); // has private fields
      return <PostCard title={post.title} body={post.body} />;
    }
  • Use context providers at the root for shared client state.
    "use client";
    
    export function ThemeProvider({ children }) {
      const [theme, setTheme] = useState("light");
      return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
          {children}
        </ThemeContext.Provider>
      );
    }
  • Wrap only interactive parts in client components to minimize bundle.
    // app/page.tsx (server)
    import LikeButton from "./LikeButton"; // only this is client
    
    export default async function Post() {
      const post = await getPost();
      return (
        <article>
          <h1>{post.title}</h1>
          <LikeButton postId={post.id} />
        </article>
      );
    }
  • Avoid importing server-only modules in client components.
    import "server-only"; // throws if imported by a client bundle

Data Fetching Patterns

  • Fetch in server components for better performance.
    export default async function Products() {
      const res = await fetch('/api/products');
      const products = await res.json();
      return (
        <ul>
          {products.map(p => <li key={p.id}>{p.name}</li>)}
        </ul>
      );
    }
  • Fetch from client for real-time or user-specific data.
    "use client";
    
    import { useEffect, useState } from "react";
    
    export function LiveFeed() {
      const [data, setData] = useState(null);
      useEffect(() => {
        fetch('/api/live-data')
          .then(r => r.json())
          .then(setData);
      }, []);
      return <div>{data?.message}</div>;
    }
  • Use SWR for client-side fetching with caching and revalidation.
    "use client";
    
    import useSWR from "swr";
    
    export function Profile({ id }) {
      const { data, error } = useSWR(`/api/users/${id}`, fetch);
      if (error) return <p>Error</p>;
      return <p>{data?.name}</p>;
    }
  • Parallel fetch multiple server requests to reduce wait time.
    export default async function Page() {
      const [user, posts] = await Promise.all([getUser(), getPosts()]);
      return <div><UserCard user={user} /><PostList posts={posts} /></div>;
    }
  • Deduplicate fetch calls with the built-in request memoization.
    // Both components call getUser() — fetched only once per request
    async function Header() { const u = await getUser(); return <p>{u.name}</p>; }
    async function Sidebar() { const u = await getUser(); return <p>{u.role}</p>; }

Component Organization

  • Separate server and client concerns into different folders.
    app/
      components/
        server/
          BlogPost.tsx   # Server component
          Header.tsx     # Server component
        client/
          Counter.tsx    # Client component
          Modal.tsx      # Client component
  • Keep client components small and focused.
    "use client";
    
    // Only interactive part is client
    export function Favorite({ postId }) {
      const [liked, setLiked] = useState(false);
      return <button onClick={() => setLiked(!liked)}>{liked ? "Liked" : "Like"}</button>;
    }
  • Colocate components near the pages that use them.
    app/
      blog/
        page.tsx
        components/
          PostCard.tsx
          PostList.tsx
  • Export reusable components from a shared ui folder.
    // app/ui/Button.tsx
    export function Button({ children, onClick }) {
      return <button className="btn" onClick={onClick}>{children}</button>;
    }
  • Use index files to simplify imports from component folders.
    // app/ui/index.ts
    export { Button } from "./Button";
    export { Card } from "./Card";
    // import { Button, Card } from "@/app/ui";

Tip: Default to server components for better performance — only use client components when you need interactivity.

Warning: "use client" at the file level makes the entire file client-side — keep client components minimal and separate.

Next.js CompilerNext.js Database Integration