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.

Tips

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

Warnings

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

Server Component Patterns

  • 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} />;
    }

Interactive Patterns

  • 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 — provide a fetcher that parses JSON.
    "use client";
    
    import useSWR from "swr";
    const fetcher = (url: string) => fetch(url).then(r => r.json());
    
    export function Profile({ id }) {
      const { data, error } = useSWR(`/api/users/${id}`, fetcher);
      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";

FAQ