Bab 2: State — Memori Komponen

5 menit baca

Kenapa Variabel Biasa Nggak Cukup?

Coba perhatiin kode ini:

jsx
function Counter() {
  let angka = 0;

  function handleKlik() {
    angka = angka + 1;
    console.log(angka); // Angka naik di console...
  }

  return (
    <div>
      <p>Angka: {angka}</p>
      <button onClick={handleKlik}>Tambah</button>
    </div>
  );
}

Kalau kamu jalanin kode ini dan klik tombolnya, yang terjadi:

  • Di console, angka naik: 1, 2, 3, 4...
  • Tapi di layar? Tetap 0. Nggak berubah.

Kenapa? Dua alasan:

  1. Variabel lokal nggak bertahan antar render. Setiap kali React me-render komponen, dia mulai dari awal. Variabel angka selalu di-set ulang ke 0.
  2. Mengubah variabel lokal nggak memicu re-render. React nggak tau kalau angka berubah, jadi dia nggak update tampilan.

Analoginya gini: bayangin kamu nulis angka di papan tulis. Setiap kali ada yang masuk ruangan (re-render), papan tulis dihapus bersih dan ditulis ulang dari awal. Variabel lokal itu kayak nulis di papan tulis yang selalu dihapus.

Yang kita butuhkan adalah sesuatu yang:

  1. Bertahan antar render (nggak di-reset)
  2. Memicu re-render saat berubah (biar tampilan update)

Itulah State.


useState: Hook Pertamamu

useState adalah sebuah Hook dari React. Hook itu fungsi spesial yang dimulai dengan kata use. Dia "mengaitkan" (hook into) fitur-fitur React ke komponen kamu.

jsx
import { useState } from 'react';

function Counter() {
  // useState mengembalikan array dengan 2 elemen:
  // 1. Nilai state saat ini
  // 2. Fungsi untuk mengubah state
  const [angka, setAngka] = useState(0);
  //     ^       ^                  ^
  //     |       |                  |
  //     |       |                  Nilai awal (initial value)
  //     |       Fungsi setter (untuk update)
  //     Nilai state saat ini

  function handleKlik() {
    setAngka(angka + 1); // Update state DAN trigger re-render!
  }

  return (
    <div>
      <p>Angka: {angka}</p>
      <button onClick={handleKlik}>Tambah</button>
    </div>
  );
}

Coba sendiri: Edit kode di bawah dan lihat hasilnya langsung!

Sekarang kalau klik tombol, angka di layar BENAR-BENAR berubah! 🎉

Apa yang Terjadi di Balik Layar?

  1. Komponen pertama kali di-render. useState(0) mengembalikan [0, setAngka]. React "mengingat" bahwa state-nya adalah 0.
  2. User klik tombol. setAngka(1) dipanggil.
  3. React tau state berubah (dari 0 ke 1), jadi dia me-render ulang komponen.
  4. Saat render ulang, useState(0) sekarang mengembalikan [1, setAngka]. Nilai 0 di parameter diabaikan karena React sudah punya nilai tersimpan (1).
  5. React update DOM sesuai hasil render baru.

Analoginya: useState itu kayak loker di gym. Kamu simpen barang (state) di loker, pergi latihan (render), balik lagi, barang masih ada. Nggak hilang kayak variabel biasa yang "dihapus" setiap render.


Anatomi useState

jsx
const [sesuatu, setSesuatu] = useState(nilaiAwal);

Mari bedah satu per satu:

Destructuring Array

useState mengembalikan array dengan tepat 2 elemen. Kita pakai array destructuring untuk memberi nama:

jsx
// Ini sama aja dengan:
const hasilUseState = useState(0);
const angka = hasilUseState[0];    // Nilai state
const setAngka = hasilUseState[1]; // Fungsi setter

// Tapi kita tulis lebih ringkas:
const [angka, setAngka] = useState(0);

Konvensi Penamaan

Polanya selalu: [sesuatu, setSesuatu]

jsx
const [nama, setNama] = useState('');
const [umur, setUmur] = useState(0);
const [aktif, setAktif] = useState(true);
const [items, setItems] = useState([]);
const [user, setUser] = useState(null);
const [formData, setFormData] = useState({ nama: '', email: '' });

Kenapa pakai set + nama? Karena ini konvensi yang dipakai semua developer React. Kalau kamu baca kode orang lain, kamu langsung tau setNama itu fungsi untuk mengubah state nama.

Nilai Awal (Initial Value)

Nilai yang kamu passing ke useState() adalah nilai awal. Bisa tipe apa aja:

jsx
useState(0)           // Number
useState('')          // String kosong
useState('Budi')     // String
useState(true)        // Boolean
useState([])          // Array kosong
useState(null)        // Null
useState({ x: 0 })   // Object

Nilai awal ini cuma dipakai di render pertama. Di render-render selanjutnya, React pakai nilai yang sudah tersimpan.


Cara State Memicu Re-render

Ini alur lengkapnya:

User klik tombol ↓ Event handler dipanggil ↓ setState() dipanggil dengan nilai baru ↓ React menandai: "Komponen ini perlu di-render ulang" ↓ React me-render ulang komponen (panggil fungsi komponen lagi) ↓ useState() mengembalikan nilai state yang BARU ↓ JSX baru dihasilkan dengan data baru ↓ React update DOM sesuai perubahan ↓ User lihat tampilan yang sudah update

Penting: setState TIDAK langsung mengubah variabel. Dia menjadwalkan re-render. Nilai state yang baru baru tersedia di render BERIKUTNYA. (Ini bakal dibahas lebih detail di bab tentang "State sebagai Snapshot".)


Multiple State Variables

Satu komponen bisa punya banyak state. Setiap useState independen satu sama lain.

jsx
import { useState } from 'react';

function ProfilUser() {
  const [nama, setNama] = useState('');
  const [umur, setUmur] = useState(0);
  const [hobi, setHobi] = useState([]);
  const [sedangEdit, setSedangEdit] = useState(false);

  return (
    <div>
      <h2>Profil</h2>
      
      {sedangEdit ? (
        <div>
          <input 
            value={nama}
            onChange={(e) => setNama(e.target.value)}
            placeholder="Nama"
          />
          <input 
            type="number"
            value={umur}
            onChange={(e) => setUmur(Number(e.target.value))}
            placeholder="Umur"
          />
          <button onClick={() => setSedangEdit(false)}>Simpan</button>
        </div>
      ) : (
        <div>
          <p>Nama: {nama || '(belum diisi)'}</p>
          <p>Umur: {umur || '(belum diisi)'}</p>
          <button onClick={() => setSedangEdit(true)}>Edit</button>
        </div>
      )}
    </div>
  );
}

Kapan Pakai Satu State vs Banyak State?

Pakai state terpisah kalau data-datanya berubah secara independen:

jsx
// ✅ Bagus — x dan y bisa berubah sendiri-sendiri
const [x, setX] = useState(0);
const [y, setY] = useState(0);

Pakai satu state (object) kalau data-datanya selalu berubah bareng:

jsx
// ✅ Bagus — firstName dan lastName biasanya diupdate bareng
const [formData, setFormData] = useState({
  firstName: '',
  lastName: '',
  email: ''
});

Aturan praktisnya: kalau kamu sering update dua state bersamaan, mungkin lebih baik digabung jadi satu object.


State Terisolasi Per Instance Komponen

Ini konsep yang sering bikin pemula bingung. Setiap instance (salinan) komponen punya state-nya sendiri yang terpisah.

jsx
import { useState } from 'react';

function Counter() {
  const [angka, setAngka] = useState(0);

  return (
    <div style={{ border: '1px solid gray', padding: '10px', margin: '10px' }}>
      <p>Angka: {angka}</p>
      <button onClick={() => setAngka(angka + 1)}>+1</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Tiga Counter Independen</h1>
      <Counter />  {/* Punya state sendiri */}
      <Counter />  {/* Punya state sendiri */}
      <Counter />  {/* Punya state sendiri */}
    </div>
  );
}

Kalau kamu klik tombol di Counter pertama, cuma Counter pertama yang berubah. Counter kedua dan ketiga tetap 0. Mereka nggak berbagi state.

Analoginya: bayangin 3 orang beda punya HP masing-masing. Kalau satu orang ganti wallpaper, HP orang lain nggak ikut berubah. Setiap instance komponen itu kayak HP terpisah — punya "memori" sendiri.

jsx
// Visualisasi mental:
// <Counter /> instance 1 → state: { angka: 5 }
// <Counter /> instance 2 → state: { angka: 0 }
// <Counter /> instance 3 → state: { angka: 12 }

Contoh Praktis: Aplikasi Todo Sederhana

jsx
import { useState } from 'react';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [inputText, setInputText] = useState('');

  function handleTambah() {
    if (inputText.trim() === '') return; // Jangan tambah kalau kosong
    
    // Buat todo baru dan tambahkan ke array
    const todoBaru = {
      id: Date.now(), // ID unik sederhana
      teks: inputText,
      selesai: false
    };
    
    setTodos([...todos, todoBaru]); // Tambah ke akhir array
    setInputText(''); // Kosongkan input
  }

  function handleToggle(id) {
    setTodos(todos.map(todo => {
      if (todo.id === id) {
        return { ...todo, selesai: !todo.selesai };
      }
      return todo;
    }));
  }

  function handleHapus(id) {
    setTodos(todos.filter(todo => todo.id !== id));
  }

  return (
    <div>
      <h2>Todo List Warung</h2>
      
      <div>
        <input
          value={inputText}
          onChange={(e) => setInputText(e.target.value)}
          placeholder="Tambah tugas..."
          onKeyDown={(e) => {
            if (e.key === 'Enter') handleTambah();
          }}
        />
        <button onClick={handleTambah}>Tambah</button>
      </div>

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span 
              style={{ 
                textDecoration: todo.selesai ? 'line-through' : 'none',
                cursor: 'pointer'
              }}
              onClick={() => handleToggle(todo.id)}
            >
              {todo.teks}
            </span>
            <button onClick={() => handleHapus(todo.id)}>❌</button>
          </li>
        ))}
      </ul>

      <p>Total: {todos.length} | Selesai: {todos.filter(t => t.selesai).length}</p>
    </div>
  );
}

Contoh Praktis: Toggle Tema Gelap/Terang

jsx
import { useState } from 'react';

function AppDenganTema() {
  const [gelap, setGelap] = useState(false);

  const style = {
    backgroundColor: gelap ? '#1a1a2e' : '#ffffff',
    color: gelap ? '#eaeaea' : '#333333',
    padding: '20px',
    minHeight: '100vh',
    transition: 'all 0.3s ease'
  };

  return (
    <div style={style}>
      <h1>Warung Kopi Digital</h1>
      <p>Selamat datang di warung kami!</p>
      
      <button onClick={() => setGelap(!gelap)}>
        {gelap ? '☀️ Mode Terang' : '🌙 Mode Gelap'}
      </button>
      
      <p>Tema saat ini: {gelap ? 'Gelap' : 'Terang'}</p>
    </div>
  );
}

Aturan Penggunaan Hook

Hook (termasuk useState) punya aturan ketat:

1. Hanya panggil di level teratas komponen

jsx
function Komponen() {
  // ✅ BENAR — di level teratas
  const [nama, setNama] = useState('');

  // ❌ SALAH — di dalam if
  if (true) {
    const [umur, setUmur] = useState(0); // JANGAN!
  }

  // ❌ SALAH — di dalam loop
  for (let i = 0; i < 3; i++) {
    const [item, setItem] = useState(''); // JANGAN!
  }

  // ❌ SALAH — di dalam fungsi nested
  function helper() {
    const [data, setData] = useState(null); // JANGAN!
  }
}

Kenapa? Karena React melacak Hook berdasarkan urutan pemanggilannya. Kalau Hook ada di dalam if atau loop, urutannya bisa berubah antar render, dan React jadi bingung Hook mana yang mana.

Analoginya: bayangin kamu punya loker bernomor di gym (loker 1, 2, 3...). React "mengingat" state berdasarkan nomor urut. Kalau tiba-tiba urutan berubah (karena if), kamu buka loker 2 tapi isinya barang loker 3. Kacau!

2. Hanya panggil di dalam komponen React atau custom Hook

jsx
// ❌ SALAH — di luar komponen
const [data, setData] = useState(null);

function BukanKomponen() {
  // ❌ SALAH — ini fungsi biasa, bukan komponen React
  const [x, setX] = useState(0);
}

// ✅ BENAR — di dalam komponen (huruf kapital)
function KomponenReact() {
  const [x, setX] = useState(0);
  return <div>{x}</div>;
}

State vs Props: Apa Bedanya?

Pemula sering bingung bedain state dan props. Ini perbandingannya:

StateProps
Siapa yang "punya"?Komponen itu sendiriParent yang passing
Bisa diubah?Ya, pakai setterTidak (read-only)
Trigger re-render?YaYa (kalau parent re-render)
AnaloginyaCatatan pribadiSurat dari atasan
jsx
// Props = data dari parent (read-only)
// State = data internal komponen (bisa diubah)

function KartuProduk({ nama, harga }) {  // props dari parent
  const [jumlah, setJumlah] = useState(0);  // state internal

  return (
    <div>
      <h3>{nama}</h3>           {/* dari props */}
      <p>Rp {harga}</p>         {/* dari props */}
      <p>Jumlah: {jumlah}</p>   {/* dari state */}
      <button onClick={() => setJumlah(jumlah + 1)}>+</button>
    </div>
  );
}

function Toko() {
  return (
    <div>
      {/* Parent passing props ke child */}
      <KartuProduk nama="Kopi Susu" harga={25000} />
      <KartuProduk nama="Es Teh" harga={10000} />
    </div>
  );
}

⚠️ Jebakan

Jebakan 1: Lupa import useState

jsx
// ❌ Error: useState is not defined
function Counter() {
  const [angka, setAngka] = useState(0);
  // ...
}

// ✅ Jangan lupa import!
import { useState } from 'react';

function Counter() {
  const [angka, setAngka] = useState(0);
  // ...
}

Jebakan 2: Langsung mutasi state

jsx
// ❌ SALAH — langsung ubah variabel state
function Salah() {
  const [angka, setAngka] = useState(0);
  
  function handleKlik() {
    angka = angka + 1; // JANGAN! Ini nggak trigger re-render
    // Dan akan error karena const
  }
}

// ✅ BENAR — pakai setter function
function Benar() {
  const [angka, setAngka] = useState(0);
  
  function handleKlik() {
    setAngka(angka + 1); // Ini yang benar
  }
}

Jebakan 3: Ekspektasi state langsung berubah

jsx
function Jebakan() {
  const [angka, setAngka] = useState(0);

  function handleKlik() {
    setAngka(5);
    console.log(angka); // Masih 0! Bukan 5!
    // State baru tersedia di render BERIKUTNYA
  }
}

Ini bakal dibahas detail di bab "State sebagai Snapshot".

Jebakan 4: Hook di dalam kondisi

jsx
// ❌ SALAH — Hook di dalam if
function Komponen({ tampilkanNama }) {
  if (tampilkanNama) {
    const [nama, setNama] = useState(''); // JANGAN!
  }
  const [umur, setUmur] = useState(0);
}

// ✅ BENAR — semua Hook di level teratas, kondisi di dalam return
function Komponen({ tampilkanNama }) {
  const [nama, setNama] = useState('');
  const [umur, setUmur] = useState(0);

  return (
    <div>
      {tampilkanNama && <p>Nama: {nama}</p>}
      <p>Umur: {umur}</p>
    </div>
  );
}

Jebakan 5: Tipe data initial value salah

jsx
// ❌ Nanti error saat .map() karena initial value bukan array
const [items, setItems] = useState('');

// ✅ Kalau mau pakai .map(), initial value harus array
const [items, setItems] = useState([]);

// ❌ Nanti error saat akses .nama karena initial value bukan object
const [user, setUser] = useState('');

// ✅ Kalau mau akses properti, initial value harus object atau null
const [user, setUser] = useState(null);
// Tapi jangan lupa handle null: user?.nama atau user && user.nama

🏋️ Challenge

Challenge 1: Lampu Toggle

Buat komponen "Lampu" yang bisa dinyalakan dan dimatikan. Tampilkan emoji 💡 kalau nyala dan 🌑 kalau mati. Tombolnya bertuliskan "Nyalakan" atau "Matikan" sesuai kondisi.

💡 Hint

Pakai useState dengan nilai boolean. true = nyala, false = mati. Toggle dengan !nilaiSekarang.

✅ Solusi
jsx
import { useState } from 'react';

function Lampu() {
  const [nyala, setNyala] = useState(false);

  function handleToggle() {
    setNyala(!nyala);
  }

  return (
    <div style={{ 
      textAlign: 'center', 
      padding: '40px',
      backgroundColor: nyala ? '#fff9c4' : '#212121',
      transition: 'all 0.3s'
    }}>
      <p style={{ fontSize: '80px' }}>
        {nyala ? '💡' : '🌑'}
      </p>
      <button onClick={handleToggle}>
        {nyala ? 'Matikan' : 'Nyalakan'}
      </button>
    </div>
  );
}

Challenge 2: Counter dengan Batas

Buat counter yang:

  • Bisa ditambah dan dikurangi
  • Nggak bisa kurang dari 0
  • Nggak bisa lebih dari 10
  • Tampilkan pesan "Maksimum!" atau "Minimum!" saat mencapai batas
💡 Hint

Pakai kondisi if di dalam handler. Cek apakah nilai baru masih dalam range sebelum memanggil setter.

✅ Solusi
jsx
import { useState } from 'react';

function CounterBerbatas() {
  const [angka, setAngka] = useState(0);
  const [pesan, setPesan] = useState('');

  function handleTambah() {
    if (angka >= 10) {
      setPesan('Maksimum! Nggak bisa lebih dari 10.');
      return;
    }
    setAngka(angka + 1);
    setPesan('');
  }

  function handleKurang() {
    if (angka <= 0) {
      setPesan('Minimum! Nggak bisa kurang dari 0.');
      return;
    }
    setAngka(angka - 1);
    setPesan('');
  }

  return (
    <div>
      <h2>Counter Berbatas (0-10)</h2>
      <p style={{ fontSize: '48px' }}>{angka}</p>
      <button onClick={handleKurang}>➖</button>
      <button onClick={handleTambah}>➕</button>
      {pesan && <p style={{ color: 'red' }}>{pesan}</p>}
    </div>
  );
}

Challenge 3: Galeri Foto Sederhana

Buat galeri yang menampilkan satu foto pada satu waktu. Ada tombol "Sebelumnya" dan "Selanjutnya" untuk navigasi. Tampilkan juga "Foto X dari Y".

💡 Hint

Simpan index foto aktif di state. Array foto bisa didefinisikan di luar state (karena nggak berubah). Navigasi = ubah index.

✅ Solusi
jsx
import { useState } from 'react';

function GaleriFoto() {
  const fotos = [
    { judul: 'Warung Kopi Pagi', url: 'https://placekitten.com/400/300' },
    { judul: 'Pasar Tradisional', url: 'https://placekitten.com/401/300' },
    { judul: 'Sawah Hijau', url: 'https://placekitten.com/402/300' },
    { judul: 'Pantai Sore', url: 'https://placekitten.com/403/300' },
  ];

  const [indexAktif, setIndexAktif] = useState(0);

  function handleSebelumnya() {
    if (indexAktif > 0) {
      setIndexAktif(indexAktif - 1);
    }
  }

  function handleSelanjutnya() {
    if (indexAktif < fotos.length - 1) {
      setIndexAktif(indexAktif + 1);
    }
  }

  const fotoSekarang = fotos[indexAktif];

  return (
    <div style={{ textAlign: 'center' }}>
      <h2>{fotoSekarang.judul}</h2>
      <img 
        src={fotoSekarang.url} 
        alt={fotoSekarang.judul}
        style={{ width: '400px', height: '300px', objectFit: 'cover' }}
      />
      <p>Foto {indexAktif + 1} dari {fotos.length}</p>
      <button 
        onClick={handleSebelumnya}
        disabled={indexAktif === 0}
      >
        ⬅️ Sebelumnya
      </button>
      <button 
        onClick={handleSelanjutnya}
        disabled={indexAktif === fotos.length - 1}
      >
        Selanjutnya ➡️
      </button>
    </div>
  );
}

Ringkasan

  • Variabel biasa nggak cukup karena: (1) di-reset setiap render, (2) nggak trigger re-render
  • useState mengembalikan [nilai, setNilai] — nilai state dan fungsi untuk mengubahnya
  • Memanggil setter (setNilai) memicu re-render komponen
  • State terisolasi per instance komponen — setiap salinan punya state sendiri
  • Hook harus dipanggil di level teratas komponen, nggak boleh di dalam if/loop
  • Konvensi penamaan: [sesuatu, setSesuatu]

Di bab selanjutnya, kita bakal menyelam lebih dalam ke proses render React. Gimana React memutuskan kapan harus update tampilan, dan apa yang sebenarnya terjadi saat kamu panggil setState.

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.