ReactJS

Belum masuk. Progresmu tidak tersimpan.

Masuk
  • Bab 7 dari 10
  • 35 menit
  • 1 latihan kode
  • 4 soal kuis

useRef, useReducer, memoisasi, dan custom hooks

Di bab ini (8 bagian)
  1. Tujuan
  2. useRef: kotak yang diingat tanpa render ulang
  3. useReducer: semua aturan perubahan di satu tempat
  4. useMemo, useCallback, dan memo
  5. Custom hooks: logika yang bisa dipakai ulang
  6. Aturan hooks
  7. Kesalahan umum
  8. Rangkuman

Tujuan bab ini

  • Memakai useRef untuk mengakses elemen DOM dan menyimpan nilai yang tidak memicu render.
  • Mengelola state dengan banyak aksi lewat useReducer dan reducer yang murni.
  • Tahu kapan useMemo, useCallback, dan memo berguna, dan kapan justru tidak perlu.
  • Membuat custom hook sendiri dan mematuhi aturan hooks.

useRef: kotak yang diingat tanpa render ulang

useRef(nilaiAwal) mengembalikan object { current: nilaiAwal } yang sama persis di setiap render. Kamu boleh mengubah ref.current kapan saja, dan React tidak merender ulang karenanya. Analoginya laci meja: kamu bisa menyimpan dan mengganti isinya, tapi tidak ada yang mengumumkan perubahan itu ke seluruh kelas.

Pemakaian pertama: mengakses elemen DOM, misalnya memfokuskan input.

import { useEffect, useRef } from "react";

function FormFokus() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // elemen <input> asli
  }, []);

  return <input ref={inputRef} placeholder="Langsung bisa ketik" />;
}

Saat render pertama inputRef.current masih null. Setelah React memasang elemen ke DOM, current diisi elemen <input>, jadi aksesnya dilakukan di efek atau event handler, bukan saat render.

Pemakaian kedua: menyimpan nilai antar render yang tidak perlu tampil, misalnya ID timer.

function Stopwatch() {
  const [detik, setDetik] = useState(0);
  const idRef = useRef(null);

  function mulai() {
    if (idRef.current) return; // sudah jalan
    idRef.current = setInterval(() => setDetik((d) => d + 1), 1000);
  }

  function berhenti() {
    clearInterval(idRef.current);
    idRef.current = null;
  }

  return (
    <>
      <p>{detik} detik</p>
      <button onClick={mulai}>Mulai</button>
      <button onClick={berhenti}>Stop</button>
    </>
  );
}

Kenapa ID timer tidak disimpan di state? Karena tidak ditampilkan, dan menyimpannya di state hanya membuat render tambahan yang sia-sia. Sebaliknya, kalau sebuah nilai ditampilkan, simpan di state, karena perubahan ref.current tidak akan memperbarui layar.

useReducer: semua aturan perubahan di satu tempat

Kalau satu state punya banyak cara berubah (tambah, kurang, reset, set ke angka tertentu), handler-mu bisa berisi logika yang tersebar di banyak tempat. useReducer mengumpulkannya di satu fungsi reducer.

import { useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "tambah":
      return { ...state, n: state.n + 1 };
    case "kurang":
      return { ...state, n: state.n - 1 };
    case "reset":
      return { ...state, n: 0 };
    default:
      return state;
  }
}

function Penghitung() {
  const [state, dispatch] = useReducer(reducer, { n: 0 });
  return (
    <>
      <p>{state.n}</p>
      <button onClick={() => dispatch({ type: "tambah" })}>+</button>
      <button onClick={() => dispatch({ type: "kurang" })}>-</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </>
  );
}

Istilahnya:

  • Action: object yang menjelaskan apa yang terjadi, biasanya { type: "tambah" }, kadang dengan data tambahan seperti { type: "hapus", id: 3 }.
  • dispatch: fungsi untuk mengirim action ke reducer.
  • Reducer: fungsi (state, action) => stateBaru. Wajib murni: tidak mengubah state lama, tidak fetch, tidak Math.random(), cukup menghitung state baru dan mengembalikannya.

Bonusnya, reducer adalah fungsi JavaScript biasa, jadi gampang diuji tanpa React: reducer({ n: 1 }, { type: "tambah" }) harus menghasilkan { n: 2 }. Itulah yang akan kamu latih di latihan bab ini.

PilihKalau
useStateData tampilan sederhana, satu atau dua cara berubah
useReducerSatu data punya banyak aksi yang saling berkaitan, logika percabangan mulai berulang
useRefNilai perlu diingat tapi tidak ditampilkan, atau butuh akses DOM

Panduan praktis: mulai dengan useState, pindah ke useReducer saat handler mulai berantakan.

useMemo, useCallback, dan memo

Ingat, setiap render memanggil ulang fungsi komponen, jadi semua perhitungan dan fungsi di dalamnya dibuat ulang. Biasanya ini tidak masalah, JavaScript cepat. Tiga alat ini untuk kasus khusus.

useMemo menyimpan hasil perhitungan dan hanya menghitung ulang bila dependency berubah.

const produkTersaring = useMemo(
  () => produk.filter((p) => p.nama.toLowerCase().includes(kata)),
  [produk, kata]
);
// menyaring ulang hanya saat produk atau kata berubah

memo membungkus komponen supaya tidak dirender ulang bila props-nya sama (dibandingkan dangkal dengan Object.is).

useCallback menyimpan referensi fungsi supaya tetap sama antar render. Ini penting justru karena memo: fungsi baru di setiap render berarti props baru, dan memo jadi tidak berguna.

const Baris = memo(function Baris({ item, onHapus }) {
  return <li>{item.nama} <button onClick={() => onHapus(item.id)}>x</button></li>;
});

function Daftar({ items, setItems }) {
  const handleHapus = useCallback((id) => {
    setItems((lama) => lama.filter((d) => d.id !== id));
  }, [setItems]);

  return <ul>{items.map((it) => <Baris key={it.id} item={it} onHapus={handleHapus} />)}</ul>;
}

Kapan perlu? Saat perhitungannya benar-benar berat (menyaring ribuan data), saat fungsi atau object dioper ke komponen yang dibungkus memo, atau saat nilainya jadi dependency efek. Kapan tidak perlu? Untuk perhitungan ringan seperti a + b, atau dipasang di semua variabel "biar aman". Memoisasi juga punya biaya (menyimpan nilai dan membandingkan dependency) dan membuat kode lebih sulit dibaca. Prinsipnya: ukur dulu dengan React DevTools Profiler, baru optimasi. Kabar baiknya, React Compiler (versi stabil 1.0 sejak Oktober 2025) bisa melakukan memoisasi otomatis, jadi kebutuhan menulis ini manual makin sedikit.

Custom hooks: logika yang bisa dipakai ulang

Custom hook adalah fungsi biasa yang namanya diawali use dan memanggil hook lain. Ia membagikan logika, bukan state: setiap komponen yang memanggilnya punya state sendiri.

// hooks/useFetch.js
export function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let batal = false;
    setLoading(true);
    fetch(url)
      .then((res) => res.json())
      .then((json) => { if (!batal) setData(json); })
      .catch((err) => { if (!batal) setError(err); })
      .finally(() => { if (!batal) setLoading(false); });
    return () => { batal = true; }; // abaikan respons lama bila url berganti
  }, [url]);

  return { data, loading, error };
}

function Produk() {
  const { data, loading, error } = useFetch("/api/produk");
  if (loading) return <p>Memuat...</p>;
  if (error) return <p>Gagal memuat</p>;
  return <ul>{data.map((p) => <li key={p.id}>{p.nama}</li>)}</ul>;
}

Contoh lain, menyimpan state ke localStorage supaya tidak hilang saat halaman di-refresh:

export function useLocalStorage(kunci, awal) {
  const [nilai, setNilai] = useState(() => {
    const tersimpan = localStorage.getItem(kunci);
    return tersimpan !== null ? JSON.parse(tersimpan) : awal;
  });

  useEffect(() => {
    localStorage.setItem(kunci, JSON.stringify(nilai));
  }, [kunci, nilai]);

  return [nilai, setNilai];
}

// const [tema, setTema] = useLocalStorage("tema", "terang");

Perhatikan useState(() => ...): fungsi inisialisasi ini hanya jalan sekali saat mount, jadi localStorage tidak dibaca di setiap render.

Aturan hooks

React mengenali setiap state berdasarkan urutan pemanggilan hook. Hook pertama selalu state nomor 1, hook kedua state nomor 2, dan seterusnya. Kalau urutannya berubah antar render, state bisa tertukar. Maka ada dua aturan wajib:

  1. Panggil hook hanya di level atas: jangan di dalam if, for, while, fungsi bersarang, atau setelah return lebih awal.
  2. Panggil hook hanya dari function component atau custom hook, bukan dari fungsi JavaScript biasa atau class component.
// SALAH: hook di dalam kondisi
if (login) {
  const [nama, setNama] = useState("");
}

// BENAR: hook selalu dipanggil, kondisinya di dalam pemakaian
const [nama, setNama] = useState("");
useEffect(() => {
  if (login) setNama("pengguna");
}, [login]);

Plugin eslint-plugin-react-hooks (aturan rules-of-hooks) menangkap pelanggaran ini otomatis.

Kesalahan umum

  • Menampilkan ref.current dan heran layar tidak berubah. Nilai yang tampil harus di state.
  • Membaca ref.current elemen saat render: nilainya masih null. Akses di efek atau handler.
  • Reducer yang memutasi state (state.n++; return state;): referensinya sama, React bisa tidak merender ulang.
  • Lupa default: return state di reducer, sehingga action tak dikenal menghasilkan undefined.
  • useMemo dan useCallback di mana-mana tanpa masalah performa yang terbukti.
  • Custom hook tanpa awalan use: linter tidak bisa memeriksa aturan hooks di dalamnya.

Rangkuman

  • useRef menyimpan nilai di ref.current tanpa memicu render, dipakai untuk akses DOM dan nilai seperti ID timer.
  • useReducer mengumpulkan semua perubahan state di reducer murni (state, action) => stateBaru, dipicu lewat dispatch.
  • useMemo menyimpan hasil hitungan, useCallback menyimpan fungsi, memo melewati render bila props sama. Pakai setelah mengukur.
  • Custom hook adalah fungsi berawalan use yang memanggil hook lain untuk berbagi logika, seperti useFetch dan useLocalStorage.
  • Hook hanya dipanggil di level atas function component atau custom hook, dengan urutan yang sama di setiap render.

Selesaikan bab ini

  1. 1. Baca materi
  2. 2. Latihan kode (1)
  3. 3. Kuis (4 soal)

Latihan kode: Reducer keranjang belanja

Komponen keranjang memakai const [state, dispatch] = useReducer(keranjangReducer, { items: [] }), dan tombolnya memanggil misalnya dispatch({ type: "tambah", produk: { id: 1, nama: "Kaos", harga: 80000 } }).

Lengkapi keranjangReducer(state, action) sebagai fungsi murni (jangan ubah state lama):

  • "tambah": bila produk dengan id itu belum ada, tambahkan { ...produk, jumlah: 1 }; bila sudah ada, naikkan jumlah-nya 1.
  • "hapus": buang item dengan action.id.
  • "ubahJumlah": set jumlah item action.id menjadi action.jumlah; bila action.jumlah 0 atau kurang, buang item itu.
  • "kosongkan": items menjadi array kosong.
  • Action lain: kembalikan state yang sama persis.

Lengkapi juga totalHarga(state) (dipakai di JSX lewat useMemo) yang menjumlahkan harga * jumlah.

Cek pemahaman

  1. 1Hook mana yang paling tepat untuk menyimpan ID dari setInterval yang tidak perlu ditampilkan?

  2. 2Apa yang ditampilkan kode berikut?

    function reducer(state, action) {
      switch (action.type) {
        case "tambah": return { n: state.n + 1 };
        case "kali": return { n: state.n * action.by };
        default: return state;
      }
    }
    
    let s = { n: 0 };
    s = reducer(s, { type: "tambah" });
    s = reducer(s, { type: "tambah" });
    s = reducer(s, { type: "kali", by: 3 });
    s = reducer(s, { type: "aneh" });
    console.log(s.n);
  3. 3Dua komponen yang sama-sama memanggil useLocalStorage("tema", "terang") otomatis berbagi satu state React yang sama.

  4. 4Kapan useCallback benar-benar berguna?