Lompat ke konten
Kembali ke modul

React: Membangun Antarmuka Pengguna · 8/9

Berbagi state: context dan reducer

Kapan prop drilling berhenti memadai, dan cara memindahkan state tanpa library tambahan.

Baca 28 menit

Setelah pelajaran ini kamu bisa

  • Mengenali kapan prop drilling sudah jadi masalah
  • Membagikan nilai dengan context, dan mengetahui biayanya
  • Menulis sebuah reducer, dan menjelaskan kenapa ia mudah diuji
  • Memilih antara `useState` dan `useReducer`

Prop drilling adalah mengirim sebuah nilai melewati component yang tidak memakainya, hanya untuk mencapai satu yang memakainya. Dua atau tiga tingkat itu wajar dan eksplisit. Enam tingkat melewati component yang mengabaikannya itu tanda masalah.

Context

tsx
import { createContext, useContext, useState } from "react";

interface ThemeValue {
  theme: "light" | "dark";
  setTheme: (t: "light" | "dark") => void;
}

// undefined as the default lets us detect "used outside a provider".
const ThemeContext = createContext<ThemeValue | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  // React 19: the context itself is the provider. No .Provider needed.
  return <ThemeContext value={{ theme, setTheme }}>{children}</ThemeContext>;
}

// A custom hook so consumers never touch the context object directly.
export function useTheme() {
  const value = useContext(ThemeContext);
  if (value === undefined) {
    throw new Error("useTheme must be used inside a ThemeProvider");
  }
  return value;
}
Hook pembungkusnya adalah bagian pentingnya: ia memberi error yang jelas alih-alih undefined misterius saat ada yang lupa provider-nya.

Reducer

Reducer adalah pure function: (stateSaatIni, action) => stateBaru. Semua logika pembaruannya berada di satu tempat, dideskripsikan sebagai data, dan karena ia pure maka sangat mudah diuji — tanpa perlu React.

Coba sendiri

Reducer sungguhan, berjalan sungguhan — ini JavaScript biasa, jadi tidak ada yang disimulasikan di sini.

Hasil

Tekan Jalankan untuk melihat hasilnya.

Ini jalan di browser kamu, di dalam sandbox. Apa pun yang kamu tulis di sini tidak bisa merusak situs.

tsx
import { useReducer } from "react";

function BookingForm() {
  const [state, dispatch] = useReducer(bookingReducer, initialState);

  return (
    <>
      <p>{state.guests.length} guests, {state.nights} nights</p>
      <button onClick={() => dispatch({ type: "guest_added", name: "Ana" })}>
        Add guest
      </button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </>
  );
}
Component-nya sekarang hanya *mendeskripsikan apa yang terjadi*. Ia tidak memuat aturan soal batas, pembatasan nilai, atau urutan — semuanya ada di reducer, diuji secara terpisah.
PakaiSaat
useStateSatu atau dua nilai independen, pembaruan sederhana
useReducerBeberapa nilai yang berubah bersama, atau ada aturan di antaranya
useReducerState berikutnya bergantung pada yang sekarang dengan cara yang tidak sederhana
useReducerKamu ingin logika pembaruannya bisa diuji tanpa render

Tugas praktik

Ambil bookingReducer dari runner-nya dan tulis file test untuknya — tanpa React, tanpa rendering, cukup expect(reducer(state, action)). Cakup: menambah saat sudah di batas, membatasi jumlah malam yang sangat besar, menghapus orang yang tidak ada, dan memastikan state input-nya tidak pernah dimutasi. Test terakhir itulah yang akan menangkap bug di masa depan.

Hasil yang diharapkan

Empat test yang lolos tanpa pernah me-render apa pun.