Next.js Cheatsheet

Server and Client Components

Use this Next.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

The Default: Server Components

Every component in app/ is a React Server Component (RSC) by default. They run on the server only — never shipped to the browser.

// app/page.tsx — Server Component (default, no directive needed)
async function fetchUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`)
  return res.json()
}

export default async function ProfilePage() {
  const user = await fetchUser('42')   // direct async/await — no useEffect
  return <h1>Hello, {user.name}</h1>
}

What you can do in Server Components: - async/await at the top level - Direct DB queries, filesystem reads, secret env vars - Import server-only modules (no bundle impact)

What you CANNOT do: - useState, useReducer, useEffect, any React hook - Browser APIs (window, document, localStorage) - Event handlers (onClick, onChange, etc.)

Client Components — 'use client'

Add 'use client' at the top of the file (before imports) to opt into the client bundle.

'use client'

import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  )
}

'use client' marks a boundary. Every component imported below it in the tree is also a client component.

Boundary Propagation Rules

LocationBehavior
No directiveServer Component
'use client' at topClient Component; everything it imports is also client
Server component imported inside a client componentNot allowed — becomes a client component too
Client component passed as children prop to a server componentAllowed — the client component stays client-side

Passing Server Data to Client Components

// app/page.tsx (Server Component)
import { ClientWidget } from '@/components/client-widget'

export default async function Page() {
  const data = await db.query('SELECT ...')  // server-only
  return <ClientWidget items={data} />        // data serialized as props
}
// components/client-widget.tsx (Client Component)
'use client'

export function ClientWidget({ items }: { items: Item[] }) {
  const [selected, setSelected] = useState<Item | null>(null)
  // ...
}

Interleaving: children Pattern

Pass a Client Component as a child of a Server Component so the server component can still fetch data.

// components/shell.tsx (Client Component)
'use client'

export function Shell({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false)
  return (
    <div>
      <button onClick={() => setOpen(o => !o)}>Toggle</button>
      {open && children}
    </div>
  )
}
// app/page.tsx (Server Component)
import { Shell } from '@/components/shell'

export default async function Page() {
  const data = await fetchExpensiveData()  // runs on server
  return (
    <Shell>
      <ServerContent data={data} />  {/* also a server component */}
    </Shell>
  )
}

server-only and client-only Packages

Prevent accidental import of server or client code in the wrong environment:

npm install server-only client-only
// lib/db.ts
import 'server-only'   // throws a build error if imported in a client component

export const db = createDb(process.env.DATABASE_URL!)
// lib/analytics.ts
import 'client-only'   // throws if imported in a server component

export function trackEvent(name: string) { /* ... */ }

Context in Client Components

Context works normally within client component trees:

'use client'

import { createContext, useContext, useState } from 'react'

const ThemeContext = createContext<'light' | 'dark'>('light')

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')
  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  )
}

export const useTheme = () => useContext(ThemeContext)

Wrap at the lowest possible level — place the Provider in a client component that wraps only what needs it:

// app/layout.tsx (Server Component — wraps with a client provider)
import { ThemeProvider } from '@/components/theme-provider'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

Third-Party Components

Libraries that use browser APIs often lack 'use client'. Wrap them:

// components/map-wrapper.tsx
'use client'

import dynamic from 'next/dynamic'

const LeafletMap = dynamic(() => import('react-leaflet').then(m => m.MapContainer), {
  ssr: false,
})

export function MapWrapper(props: MapProps) {
  return <LeafletMap {...props} />
}

Comparison Table

FeatureServer ComponentClient Component
Directive(none)'use client'
async/awaitYesNo (use useEffect / SWR)
React hooksNoYes
Browser APIsNoYes
DB / secretsYesNo
Bundle size impactNoneYes
Rendered whereServer onlyServer + hydrated client
Default in app/YesNo

'use cache' Directive (Cache Components, Next 16 beta)

The 'use cache' directive (not a function) caches a file, component, or function. Enable Cache Components in config first:

// next.config.ts
const config: NextConfig = { cacheComponents: true }
// Function-level: cache one async function
import { cacheLife, cacheTag } from 'next/cache'

export async function getUser(id: string) {
  'use cache'
  cacheLife('hours')          // profile: 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'max'
  cacheTag(`user-${id}`)      // tag for targeted invalidation
  return db.users.findUnique({ where: { id } })
}
// Component-level: cache a Server Component's rendered output
export async function ProductCard({ id }: { id: string }) {
  'use cache'
  const product = await fetchProduct(id)
  return <article>{product.name}</article>
}
// File-level: first line of the file — everything exported is cached
'use cache'

Invalidate by tag from a Server Action or Route Handler:

import { revalidateTag } from 'next/cache'

revalidateTag('user-42')

Do not confuse this with unstable_cache() from next/cache — that is the older wrapper-function API. 'use cache' + cacheLife/cacheTag is the Next 15/16 model.

Serialization Gotchas

Props passed from Server → Client Components must be serializable by React (React 19 RSC serialization is richer than JSON):

  • Allowed: strings, numbers, BigInt, booleans, undefined, null, NaN/Infinity, plain objects, arrays, Date, Map, Set, TypedArray/ArrayBuffer, FormData, JSX elements, Promises (stream to the client; unwrap with React's use()), and Server Functions ('use server')
  • Not allowed: regular functions/closures, class instances, symbols (except Symbol.for(...))