Berbagi State Antar Komponen

Terkadang, Anda ingin dua komponen selalu berubah secara bersamaan. Untuk melakukannya, hapus state dari kedua komponen, pindahkan ke komponen induk terdekat, dan kemudian oper ke komponen tersebut melalui props. Ini dikenal sebagai mengangkat state ke atas, dan ini adalah salah satu hal yang paling umum yang akan Anda lakukan saat menulis kode React.

You will learn

  • Bagaimana cara berbagi state antar komponen dengan menaikkan state ke atas (lifting state up)
  • Apa itu komponen terkendali (controlled component) dan tak terkendali (uncontrolled component)

Contoh Mengangkat State Keatas

Pada contoh ini, komponen induk Accordion merender dua komponen Panel terpisah:

  • Accordion
    • Panel
    • Panel

Setiap komponen Panel memiliki state boolean isActive yang menentukan apakah kontennya terlihat.

Tekan tombol Tampilkan untuk kedua panel:

import { useState } from 'react';

function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false);
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Tampilkan
        </button>
      )}
    </section>
  );
}

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel title="Tentang">
        Dengan populasi sekitar 2 juta, Almaty adalah kota terbesar di Kazakhstan. Dari tahun 1929 hingga 1997, itu adalah ibu kota negara tersebut.
      </Panel>
      <Panel title="Etimologi">
        Nama berasal dari <span lang="kk-KZ">алма</span>, kata Kazakh untuk "apel" dan sering diterjemahkan sebagai "penuh dengan apel". Faktanya, wilayah sekitar Almaty dipercaya sebagai rumah leluhur apel, dan tanaman liar <i lang="la">Malus sieversii</i> dianggap sebagai kandidat yang mungkin untuk leluhur apel domestik modern.
      </Panel>
    </>
  );
}

Perhatikan bagaimana menekan tombol satu panel tidak memengaruhi panel lainnya—mereka independen.

Diagram yang menunjukkan pohon dari tiga komponen, satu induk yang diberi label Accordion dan dua anak yang diberi label Panel dan dua komponen anak yang berlabel Panel. Kedua komponen Panel berisi isActive dengan nilai false.
Diagram yang menunjukkan pohon dari tiga komponen, satu induk yang diberi label Accordion dan dua anak yang diberi label Panel dan dua komponen anak yang berlabel Panel. Kedua komponen Panel berisi isActive dengan nilai false.

Awalnya, setiap Panel memiliki state isActive dengan nilai false, sehingga keduanya terlihat tertutup

Diagram yang sama seperti sebelumnya, dengan isActive dari komponen Panel anak pertama yang disorot menunjukkan klik dengan nilai isActive diatur ke true. Komponen Panel anak kedua masih berisi nilai false.
Diagram yang sama seperti sebelumnya, dengan isActive dari komponen Panel anak pertama yang disorot menunjukkan klik dengan nilai isActive diatur ke true. Komponen Panel anak kedua masih berisi nilai false.

Menekan tombol Panel mana pun hanya akan memperbarui state isActive dari Panel itu sendiri

Tetapi sekarang katakanlah Anda ingin mengubah panel tersebut sehingga hanya satu panel yang dibuka pada satu waktu. Dengan desain di atas, membuka panel kedua berarti secara otomatis menutup panel pertama. Bagaimanakah Anda akan melakukannya?

Untuk mengkoordinasikan kedua panel ini, Anda perlu “mengangkat state mereka” ke komponen induk dalam tiga langkah:

  1. Hapus state dari komponen anak.
  2. Oper data yang dituliskan langsung di dalam kode (hardcoded) dari komponen induk.
  3. Tambahkan state ke komponen induk dan oper bersamaan dengan event handlers.

Cara ini akan memungkinkan komponen Accordion untuk mengkoordinasikan kedua Panel dan hanya membuka satu panel pada satu waktu.

Langkah 1: Hapus state dari komponen anak

Anda akan memberikan kontrol isActive dari Panel ke komponen induknya. Ini berarti komponen induk akan mengoper isActive ke Panel sebagai prop. Mulai dengan menghapus baris ini dari komponen Panel:

const [isActive, setIsActive] = useState(false);

Lalu, tambahkan isActive ke daftar prop Panel:

function Panel({ title, children, isActive })

Sekarang komponen induk Panel dapat mengontrol isActive dengan mengoper sebagai prop. Sebaliknya, komponen Panel sekarang tidak memiliki kontrol atas nilai isActive—sekarang terserah komponen induk!

Langkah 2: Oper data yang dituliskan langsung di dalam kode dari komponen induk

Untuk mengangkat state, Anda harus menemukan komponen induk yang paling dekat dari kedua komponen anak yang ingin Anda koordinasikan:

  • Accordion (komponen induk terdekat)
    • Panel
    • Panel

Pada contoh ini, komponen Accordion adalah yang terdekat. Karena komponen ini berada di atas kedua panel dan dapat mengontrol prop mereka, komponen ini akan menjadi “sumber kebenaran” untuk panel mana yang sedang aktif. Buat komponen Accordion mengoper nilai isActive yang telah ditentukan sebelumnya (misalnya, true) ke kedua panel:

import { useState } from 'react';

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel title="Tentang" isActive={true}>
        Dengan populasi sekitar 2 juta, Almaty adalah kota terbesar di Kazakhstan. Dari tahun 1929 hingga 1997, itu adalah ibu kota negara tersebut.
      </Panel>
      <Panel title="Etimologi" isActive={true}>
        Nama berasal dari <span lang="kk-KZ">алма</span>, kata Kazakh untuk "apel" dan sering diterjemahkan sebagai "penuh dengan apel". Faktanya, wilayah sekitar Almaty dipercaya sebagai rumah leluhur apel, dan <i lang="la">Malus sieversii</i> si liar dianggap sebagai kandidat yang mungkin untuk leluhur apel domestik modern.
      </Panel>
    </>
  );
}

function Panel({ title, children, isActive }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Tampilkan
        </button>
      )}
    </section>
  );
}

Coba ubah nilai isActive yang dituliskan langsung di dalam kode komponen Accordion dan lihat hasilnya di layar.

Langkah 3: Tambahkan state ke komponen induk

Memindahkan state ke atas seringkali mengubah sifat dari apa yang Anda simpan sebagai state.

Dalam contoh ini, Anda ingin mengubah Accordion sehingga hanya satu panel yang dapat dibuka pada satu waktu. Ini berarti bahwa komponen induk Accordion perlu melacak panel mana yang sedang aktif. Alih-alih nilai boolean, ia dapat menggunakan angka sebagai indeks Panel aktif untuk variabel state:

const [activeIndex, setActiveIndex] = useState(0);

Ketika activeIndex bernilai 0, panel pertama aktif, dan ketika bernilai 1, panel kedua aktif.

Menekan tombol “Tampilkan” di salah satu Panel perlu mengubah indeks aktif di Accordion. Sebuah Panel tidak dapat mengatur state activeIndex secara langsung karena ia didefinisikan di dalam Accordion. Komponen Accordion perlu secara ekplisit mengizinkan komponen Panel untuk mengubah state-nya dengan mengoper event handler sebagai prop:

<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>

Elemen <button> di dalam Panel sekarang akan menggunakan prop onShow sebagai event handler kliknya:

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="Tentang"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        Dengan populasi sekitar 2 juta, Almaty adalah kota terbesar di Kazakhstan. Dari tahun 1929 hingga 1997, itu adalah ibu kota negara tersebut.
      </Panel>
      <Panel
        title="Etimologi"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
       Nama berasal dari <span lang="kk-KZ">алма</span>, kata Kazakh untuk "apel" dan sering diterjemahkan sebagai "penuh dengan apel". Faktanya, wilayah sekitar Almaty dipercaya sebagai rumah leluhur apel, dan tanaman liar <i lang="la">Malus sieversii</i> dianggap sebagai kandidat yang mungkin untuk leluhur apel domestik modern.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Tampilkan
        </button>
      )}
    </section>
  );
}

Dengan ini, terselesaikanlah pengangkatan state ke atas! Memindahkan state ke komponen induk memungkinkan Anda untuk mengkoordinasikan kedua panel. Menggunakan indeks aktif alih-alih dua flag “ditampilkan” memastikan bahwa hanya satu panel yang aktif pada satu waktu. Dan mengoper event handler ke komponen anak memungkinkan komponen anak untuk mengubah state induknya.

Diagram yang menunjukkan pohon tiga komponen, satu induk yang diberi label Accordion dan dua anak yang diberi label Panel. Accordion berisi nilai activeIndex nol yang berubah menjadi nilai isActive true yang dilewatkan ke Panel pertama, dan nilai isActive false yang dioper ke Panel kedua.
Diagram yang menunjukkan pohon tiga komponen, satu induk yang diberi label Accordion dan dua anak yang diberi label Panel. Accordion berisi nilai activeIndex nol yang berubah menjadi nilai isActive true yang dilewatkan ke Panel pertama, dan nilai isActive false yang dioper ke Panel kedua.

Awalnya, nilai activeIndex dari Accordion adalah 0, sehingga Panel pertama menerima isActive = true

Diagram yang sama dengan sebelumnya, dengan nilai activeIndex dari komponen induk Accordion yang diberi sorotan menunjukkan klik dengan nilai yang berubah menjadi satu. Aliran ke kedua komponen Panel anak juga disorot, dan nilai isActive yang dioper ke setiap anak diatur ke yang berlawanan: false untuk Panel pertama dan true untuk yang kedua.
Diagram yang sama dengan sebelumnya, dengan nilai activeIndex dari komponen induk Accordion yang diberi sorotan menunjukkan klik dengan nilai yang berubah menjadi satu. Aliran ke kedua komponen Panel anak juga disorot, dan nilai isActive yang dioper ke setiap anak diatur ke yang berlawanan: false untuk Panel pertama dan true untuk yang kedua.

Ketika nilai activeIndex dari Accordion berubah menjadi 1, Panel kedua menerima isActive = true sebagai gantinya

Deep Dive

Komponen terkendali dan tak terkendali

Secara umum Anda dapat menyebut sebuah komponen yang memiliki state lokal sebagai “tak terkendali”. Misalnya, komponen Panel yang asli dengan variabel state isActive adalah tak terkendali karena induknya tidak dapat mempengaruhi apakah panel aktif atau tidak.

Sebaliknya, Anda dapat menyebut sebuah komponen “terkendali” ketika informasi penting di dalamnya dikendalikan oleh prop daripada state lokalnya sendiri. Ini memungkinkan komponen induk untuk sepenuhnya menentukan perilakunya. Komponen Panel akhir dengan prop isActive dikendalikan oleh komponen Accordion.

Komponen-komponen tak terkendali lebih mudah digunakan dalam induknya karena membutuhkan konfigurasi yang lebih sedikit. Tetapi mereka kurang fleksibel ketika Anda ingin mengkoordinasikannya bersamaan. Komponen-komponen terkendali sepenuhnya fleksibel, tetapi mereka membutuhkan komponen induk untuk sepenuhnya mengkonfigurasi mereka dengan props.

Pada praktiknya, “terkendali” dan “tak terkendali” bukanlah istilah teknis yang ketat—setiap komponen biasanya memiliki beberapa campuran dari kedua state lokal dan props. Namun, ini adalah cara yang berguna untuk berbicara tentang bagaimana komponen dirancang dan kemampuan apa yang mereka tawarkan.

Ketika menulis komponen, pertimbangkan informasi manakah di dalamnya yang seharusnya dikendalikan (melalui props), dan informasi manakah yang seharusnya tak terkendali (melalui state). Tetapi Anda selalu dapat mengubah pikiran Anda dan melakukan refaktorasi belakangan.

Satu sumber kebenaran untuk setiap state

Pada aplikasi React, banyak komponen akan memiliki state mereka sendiri. Beberapa state mungkin “hidup” dekat dengan komponen daun (komponen di bagian bawah pohon) seperti masukan-masukan (inputs). State lainnya mungkin “hidup” lebih dekat ke bagian atas aplikasi. Misalnya, bahkan pustaka-pustaka rute pada sisi klien (client-side routing libraries) biasanya diimplementasikan dengan menyimpan rute saat ini di state React, dan mengopernya dengan props!

Untuk setiap potongan state yang unik, Anda akan memilih komponen yang “memilikinya”. Prinsip ini juga dikenal sebagai memiliki “sumber kebenaran tunggal”. Ini tidak berarti bahwa semua state berada di satu tempat—tetapi bahwa untuk setiap potongan state, ada komponen tertentu yang memegang potongan informasi itu. Alih-alih menduplikasi state yang sama diantara komponen, angkatlah state tersebut ke induk mereka, dan oper ke anak-anak yang membutuhkannya.

Aplikasi Anda akan berubah saat Anda mengerjakannya. Biasanya Anda akan memindahkan state ke bawah atau kembali ke atas saat Anda masih mencari tahu di mana setiap potongan state “hidup”. Semua ini adalah bagian dari proses!

Untuk mengetahui bagaimana penerapannya dalam praktik dengan beberapa komponen lebih banyak, baca Berpikir dalam React.

Recap

  • Ketika Anda ingin mengkoordinasikan dua komponen, pindahkan state mereka ke induknya.
  • Kemudian oper informasi yang diperlukan melalui props dari induk mereka.
  • Terakhir, oper event handler sehingga komponen anak-anak dapat mengubah state induk.
  • Dapat membantu jika Anda mempertimbangkan komponen sebagai “terkendali” (dikendalikan oleh props) atau “tak terkendali” (dikendalikan oleh state).

Challenge 1 of 2:
Masukan yang disinkronkan

Berikut ini dua masukan yang independen. Buat mereka tetap disinkronkan: mengedit satu masukan harus memperbarui masukan lain dengan teks yang sama, dan sebaliknya.

import { useState } from 'react';

export default function SyncedInputs() {
  return (
    <>
      <Input label="masukan pertama" />
      <Input label="masukan kedua" />
    </>
  );
}

function Input({ label }) {
  const [text, setText] = useState('');

  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <label>
      {label}
      {' '}
      <input
        value={text}
        onChange={handleChange}
      />
    </label>
  );
}