nextjscontextstateMahir4 mnt baca

Context di Era Server Components

React Context hanya untuk client: pola provider yang benar.

Masalah: context butuh runtime client

React Context (createContext + useContext) adalah mekanisme re-render reaktif: ketika value berubah, semua consumer me-render ulang. Mekanisme itu hanya hidup di browser. Server Component me-render sekali menjadi HTML lalu selesai, tidak ada proses re-render atau subscription di sana. Akibatnya, memanggil useContext langsung di Server Component menghasilkan error.

Ini sering membingungkan pemula yang terbiasa memakai context untuk "state global" di React biasa. Di era Server Components, state global terbelah dua: data server (diambil lewat fetch/query langsung di Server Component, bukan context) dan UI state client (theme, bahasa, isi keranjang versi tampilan) yang memang butuh context.

Pola yang benar: provider client di layout server

  1. Buat provider sebagai Client Component ("use client").
  2. Pasang provider di root layout. Layout boleh server, karena Server Component boleh me-render Client Component.

Contoh 1: ThemeProvider lengkap dengan consumer

tsx
// app/providers.tsx
"use client";

import { createContext, useContext, useState, type ReactNode } from "react";

type Theme = { dark: boolean; toggle: () => void };

const ThemeCtx = createContext<Theme | null>(null);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [dark, setDark] = useState(false);

  return (
    <ThemeCtx.Provider value={{ dark, toggle: () => setDark((d) => !d) }}>
      {children}
    </ThemeCtx.Provider>
  );
}

export function useThemeCtx() {
  const ctx = useContext(ThemeCtx);
  if (!ctx) throw new Error("useThemeCtx harus dipakai di dalam ThemeProvider");
  return ctx;
}
tsx
// app/layout.tsx (tetap server, hanya "memakai" provider client)
import { ThemeProvider } from "./providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="id">
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}
tsx
// app/ThemeToggle.tsx (consumer: wajib client juga)
"use client";

import { useThemeCtx } from "./providers";

export default function ThemeToggle() {
  const { dark, toggle } = useThemeCtx();
  return (
    <button onClick={toggle} aria-pressed={dark}>
      {dark ? "☀️ Mode terang" : "🌙 Mode gelap"}
    </button>
  );
}

Perhatikan rantainya: provider client dipasang di layout server, consumer (ThemeToggle) juga client. Server Component boleh dibungkus provider (children lewat begitu saja), tapi tidak boleh memakai useContext. Guard if (!ctx) throw membuat error-nya jelas kalau hook dipakai di luar provider, jauh lebih membantu daripada cannot read property of null.

Contoh 2: banyak provider digabung rapi

Aplikasi nyata butuh beberapa context (theme, bahasa, keranjang). Jangan menumpuknya di layout sampai tidak terbaca:

tsx
// app/providers.tsx (gabungan)
"use client";

import type { ReactNode } from "react";
import { ThemeProvider } from "./theme-provider";
import { LanguageProvider } from "./language-provider";
import { CartProvider } from "./cart-provider";

export function AppProviders({ children }: { children: ReactNode }) {
  return (
    <ThemeProvider>
      <LanguageProvider>
        <CartProvider>{children}</CartProvider>
      </LanguageProvider>
    </ThemeProvider>
  );
}
tsx
// app/cart-provider.tsx
"use client";

import { createContext, useContext, useMemo, useState, type ReactNode } from "react";

type CartCtx = { count: number; add: () => void };
const Ctx = createContext<CartCtx | null>(null);

export function CartProvider({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);
  const value = useMemo(() => ({ count, add: () => setCount((c) => c + 1) }), [count]);
  return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
}

export function useCart() {
  const ctx = useContext(Ctx);
  if (!ctx) throw new Error("useCart harus dipakai di dalam CartProvider");
  return ctx;
}

useMemo pada value mencegah seluruh consumer re-render setiap kali provider me-render ulang karena alasan lain. Layout tinggal memakai satu <AppProviders>, dan menambah context baru tidak mengotori layout.

Kapan JANGAN pakai context

Godaan terbesar: memasukkan data server (daftar produk, profil user) ke context agar "bisa diakses di mana saja". Jangan. Data server diambil langsung di Server Component yang membutuhkannya lewat fetch atau query. Context dicadangkan untuk state yang benar-benar reaktif di client: theme, bahasa tampilan, sidebar terbuka/tutup, atau draf form.

Tantangan

Provider bahasa

Buat LanguageProvider (client) dengan context ID/EN, pasang di root layout, dan buat tombol toggle di satu Client Component. Pastikan Server Component tidak memanggil useContext langsung.

Kuis Bab

Uji pemahamanmu: Server & Client Components

Jawab 5 soal berikut, lalu tekan "Periksa Jawaban".

1.Kapan kamu HARUS memakai Client Component?

2.Apa yang TIDAK boleh dioper dari Server ke Client Component?

3.Bagaimana cara memakai Server Component di dalam Client Component?

4.Di mana sebaiknya menaruh "use client"?

5.Bagaimana pola Context yang benar di App Router?