Cara Berpikir dengan React

React dapat mengubah cara berpikir Anda tentang desain yang Anda lihat dan aplikasi yang Anda buat. Ketika Anda membuat antarmuka pengguna dengan React, pertama-tama Anda akan memecahnya menjadi beberapa bagian yang disebut dengan komponen. Kemudian, Anda akan mendeskripsikan state visual yang berbeda untuk setiap komponen Anda. Terakhir, Anda akan menghubungkan komponen-komponen Anda bersama-sama sehingga data mengalir melaluinya. Dalam tutorial ini, kami akan memandu Anda melalui proses berpikir untuk membangun tabel data produk yang dapat dicari dengan React.

Mulailah dengan sebuah rancang bangun

Bayangkan Anda sudah memiliki API JSON dan rancang bangun dari desainer.

API JSON mengembalikan beberapa data yang terlihat seperti ini:

[
{ category: "Fruits", price: "$1", stocked: true, name: "Apple" },
{ category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit" },
{ category: "Fruits", price: "$2", stocked: false, name: "Passionfruit" },
{ category: "Vegetables", price: "$2", stocked: true, name: "Spinach" },
{ category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin" },
{ category: "Vegetables", price: "$1", stocked: true, name: "Peas" }
]

Rancang bangun kita terlihat seperti ini:

Untuk mengimplementasikan antarmuka pengguna di React, Anda biasanya akan mengikuti lima langkah yang sama.

Langkah 1: Bagi antarmuka pengguna menjadi hierarki komponen

Mulailah dengan menggambar kotak-kotak di sekitar setiap komponen dan subkomponen dalam rancang bangun dan beri nama. Jika Anda bekerja dengan seorang desainer, mereka mungkin telah menamai komponen-komponen ini di alat bantu desain mereka. Tanyakan kepada mereka!

Tergantung pada latar belakang Anda, Anda dapat berpikir untuk membagi desain menjadi beberapa komponen dengan cara yang berbeda:

  • Pemrograman—gunakan teknik yang sama untuk memutuskan apakah Anda harus membuat fungsi atau objek baru. Salah satu teknik tersebut adalah single responsibility principle, yaitu sebuah komponen idealnya hanya melakukan satu hal. Jika komponen tersebut berkembang, maka harus dipecah menjadi subkomponen yang lebih kecil.
  • CSS—pertimbangkan untuk apa Anda akan membuat class selector. (Namun, komponen tidak terlalu terperinci.)
  • Desain—pertimbangkan bagaimana Anda akan mengatur layer desain.

Jika JSON Anda terstruktur dengan baik, Anda akan sering menemukan bahwa JSON tersebut secara alami memetakan struktur komponen UI Anda. Hal ini karena UI dan model data sering kali memiliki arsitektur informasi yang sama—atau, bentuk yang sama. Pisahkan UI Anda menjadi beberapa komponen, di mana setiap komponen cocok dengan satu bagian dari model data Anda.

Terdapat lima komponen pada layar ini:

  1. FilterableProductTable (abu-abu) berisi seluruh aplikasi.
  2. SearchBar (biru) menerima masukan dari pengguna.
  3. ProductTable (lavender) menampilkan dan memfilter list sesuai dengan masukan pengguna.
  4. ProductCategoryRow (hijau) menampilkan judul untuk setiap kategori.
  5. ProductRow (kuning) menampilkan baris untuk setiap produk.

Jika Anda melihat ProductTable (lavender), Anda akan melihat bahwa header tabel (yang berisi label “Name” dan “Price”) bukan merupakan komponennya sendiri. Ini adalah masalah preferensi, dan Anda dapat memilih salah satu. Dalam contoh ini, header tersebut merupakan bagian dari ProductTable karena muncul di dalam list ProductTable. Namun, jika header ini menjadi kompleks (misalnya, jika Anda menambahkan pengurutan), Anda dapat memindahkannya ke dalam komponen ProductTableHeader sendiri.

Setelah Anda mengidentifikasi komponen-komponen dalam rancang bangun, susunlah komponen-komponen tersebut ke dalam sebuah hirarki. Komponen yang muncul di dalam komponen lain dalam rancang bangun harus muncul sebagai anak dalam hierarki:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

Langkah 2: Buat versi statis di React

Setelah Anda memiliki hierarki komponen, sekarang saatnya mengimplementasikan aplikasi Anda. Pendekatan yang paling mudah adalah membuat versi yang merender UI dari model data Anda tanpa menambahkan interaktivitas apa pun… untuk sementara! Sering kali lebih mudah untuk membuat versi statis terlebih dahulu dan menambahkan interaktivitas kemudian. Membangun versi statis membutuhkan banyak pengetikan dan tidak perlu berpikir, tetapi menambahkan interaktivitas membutuhkan banyak pemikiran dan tidak perlu banyak pengetikan.

Untuk membuat versi statis dari aplikasi Anda yang merender model data Anda, Anda perlu membuat komponen yang menggunakan kembali komponen lain dan mengirimkan data menggunakan props. Props adalah cara untuk mengoper data dari induk ke anak. (Jika Anda sudah terbiasa dengan konsep state, jangan gunakan state sama sekali untuk membangun versi statis ini. State hanya diperuntukkan bagi interaktivitas, yaitu data yang berubah seiring waktu. Karena ini adalah versi statis dari aplikasi, Anda tidak memerlukannya).

Anda bisa membangun “dari atas ke bawah” dengan memulai membangun komponen yang lebih tinggi dalam hierarki (seperti FilterableProductTable) atau “dari bawah ke atas” dengan bekerja dari komponen yang lebih rendah (seperti ProductRow). Dalam contoh yang lebih sederhana, biasanya lebih mudah untuk bekerja dari atas ke bawah, dan pada proyek yang lebih besar, lebih mudah untuk bekerja dari bawah ke atas.

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

function ProductTable({ products }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (product.category !== lastCategory) {
      rows.push(
        <ProductCategoryRow
          category={product.category}
          key={product.category} />
      );
    }
    rows.push(
      <ProductRow
        product={product}
        key={product.name} />
    );
    lastCategory = product.category;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

function SearchBar() {
  return (
    <form>
      <input type="text" placeholder="Search..." />
      <label>
        <input type="checkbox" />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

function FilterableProductTable({ products }) {
  return (
    <div>
      <SearchBar />
      <ProductTable products={products} />
    </div>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

(Jika kode ini terlihat menyeramkan, bacalah Quick Start terlebih dahulu!)

Setelah membuat komponen, Anda akan memiliki pustaka komponen yang dapat digunakan kembali untuk merender model data Anda. Karena ini adalah aplikasi statis, komponen-komponennya hanya akan mengembalikan JSX. Komponen di bagian atas hirarki (FilterableProductTable) akan mengambil model data Anda sebagai props. Ini disebut aliran data satu arah (one-way data flow) karena data mengalir turun dari komponen tingkat atas ke komponen di bagian bawah pohon.

Pitfall

Pada titik ini, Anda tidak perlu menggunakan nilai state apa pun. Itu untuk langkah selanjutnya!

Langkah 3: Identifikasi representasi minimal namun komplit dari state UI

Untuk membuat UI interaktif, Anda harus mengizinkan pengguna mengubah model data yang mendasarinya. Anda akan menggunakan state untuk ini.

Bayangkan state sebagai kumpulan data perubahan minimal yang perlu diingat oleh aplikasi Anda. Prinsip paling penting dalam menyusun state adalah menjaganya agar tetap [DRY (Don’t Repeat Yourself)] (https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) Cari tahu representasi minimal absolut dari state yang dibutuhkan aplikasi Anda dan hitung semua yang lain sesuai permintaan. Sebagai contoh, jika Anda membuat daftar belanja, Anda dapat menyimpan item sebagai array dalam state. Jika Anda juga ingin menampilkan jumlah item dalam daftar, jangan simpan jumlah item sebagai nilai state lain—sebagai gantinya, baca panjang senarai Anda.

Sekarang pikirkan semua bagian data dalam contoh aplikasi ini:

  1. Daftar produk asli
  2. Teks pencarian yang dimasukkan pengguna
  3. Nilai dari kotak centang
  4. Daftar produk yang difilter

Manakah yang termasuk state? Identifikasi mana yang bukan:

  • Apakah data tetap tidak berubah dari waktu ke waktu? Jika ya, data tersebut bukan state.
  • Apakah data diturunkan dari induk melalui props? Jika ya, data tersebut bukan state.
  • Bisakah Anda menghitungnya berdasarkan state atau props yang ada di komponen Anda? Jika iya, maka data tersebut pasti bukan state!

Yang tersisa mungkin adalah state.

Mari kita lihat satu per satu lagi:

  1. Daftar produk asli dioper sebagai props, jadi bukan merupakan state.
  2. Teks pencarian tampaknya adalah state karena berubah dari waktu ke waktu dan tidak dapat dihitung dari apa pun.
  3. Nilai kotak centang tampaknya adalah state karena berubah dari waktu ke waktu dan tidak dapat dihitung dari apa pun.
  4. Daftar produk yang difilter bukan state karena dapat dihitung dengan mengambil daftar produk asli dan memfilternya sesuai dengan teks pencarian dan nilai kotak centang.

Ini berarti, hanya teks pencarian dan nilai kotak centang yang merupakan state! Bagus sekali!

Deep Dive

Props vs State

Ada dua jenis data “model” dalam React: props dan state. Keduanya sangat berbeda:

  • Props seperti argumen yang Anda berikan ke sebuah fungsi. Mereka memungkinkan komponen induk mengoper data ke komponen anak dan menyesuaikan tampilannya. Sebagai contoh, sebuah Form dapat mengoper sebuah props color ke sebuah Button.
  • State seperti memori sebuah komponen. Memungkinkan sebuah komponen melacak beberapa informasi dan mengubahnya sebagai respons terhadap interaksi. Sebagai contoh, sebuah Button dapat melacak state isHovered.

Props dan state berbeda, tetapi keduanya bekerja bersama. Komponen induk akan sering menyimpan beberapa informasi dalam state (sehingga dapat mengubahnya), dan meneruskannya ke komponen anak sebagai props mereka. Tidak apa-apa jika perbedaannya masih terasa kabur saat pertama kali dibaca. Dibutuhkan sedikit latihan agar benar-benar melekat!

Step 4: Identifikasi dimana state Anda berada

Setelah mengidentifikasi data state minimal aplikasi Anda, Anda perlu mengidentifikasi komponen mana yang bertanggung jawab untuk mengubah state ini, atau memiliki state tersebut. Ingat: React menggunakan aliran data satu arah, mengoper data turun melalui hirarki komponen dari komponen induk ke komponen anak. Mungkin tidak langsung jelas komponen mana yang harus memiliki state apa. Hal ini dapat menjadi tantangan jika Anda baru mengenal konsep ini, tetapi Anda dapat mengetahuinya dengan mengikuti langkah-langkah berikut ini!

Untuk setiap bagian state dalam aplikasi Anda:

  1. Identifikasi setiap komponen yang merender sesuatu berdasarkan state tersebut.
  2. Temukan komponen induk yang paling dekat—komponen yang berada di atas semua komponen dalam hirarki.
  3. Tentukan di mana state tersebut harus berada:
    1. Sering kali, Anda dapat meletakkan state secara langsung ke dalam induknya.
    2. Anda juga dapat menempatkan state ke dalam beberapa komponen di atas induknya.
    3. Jika Anda tidak dapat menemukan komponen yang masuk akal untuk memiliki state, buatlah komponen baru hanya untuk menyimpan state dan tambahkan di suatu tempat di dalam hirarki di atas komponen induk umum.

Pada langkah sebelumnya, Anda menemukan dua bagian status dalam aplikasi ini: teks input pencarian, dan nilai kotak centang. Dalam contoh ini, keduanya selalu muncul bersamaan, sehingga masuk akal untuk meletakkannya di tempat yang sama.

Sekarang mari kita bahas strateginya:

  1. Identifikasi komponen yang menggunakan state:
    • ProductTable perlu memfilter daftar produk berdasarkan status tersebut (teks pencarian dan nilai kotak centang).
    • SearchBar perlu menampilkan status tersebut (teks pencarian dan nilai kotak centang).
  2. Temukan induk yang sama: Komponen induk pertama yang dimiliki oleh kedua komponen tersebut adalah FilterableProductTable.
  3. Tentukan di mana state berada: Kita akan menyimpan teks filter dan nilai state kotak centang di FilterableProductTable.

Jadi nilai state akan berada di dalam FilterableProductTable.

Tambahkan state ke komponen menggunakan Hook useState(). Hook adalah fungsi khusus yang memungkinkan Anda “mengaitkan ke dalam” React. Tambahkan dua variabel state di bagian atas FilterableProductTable dan tentukan state awalnya:

function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);

Kemudian, berikan filterText dan inStockOnly ke ProductTable dan SearchBar sebagai props:

<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly} />
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly} />
</div>

Anda dapat mulai melihat bagaimana aplikasi Anda akan berperilaku. Edit nilai awal filterText dari useState('') menjadi useState('fruit') pada kode sandbox di bawah ini. Anda akan melihat teks input pencarian dan tabel diperbarui:

import { useState } from 'react';

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

  return (
    <div>
      <SearchBar 
        filterText={filterText} 
        inStockOnly={inStockOnly} />
      <ProductTable 
        products={products}
        filterText={filterText}
        inStockOnly={inStockOnly} />
    </div>
  );
}

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

function ProductTable({ products, filterText, inStockOnly }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (
      product.name.toLowerCase().indexOf(
        filterText.toLowerCase()
      ) === -1
    ) {
      return;
    }
    if (inStockOnly && !product.stocked) {
      return;
    }
    if (product.category !== lastCategory) {
      rows.push(
        <ProductCategoryRow
          category={product.category}
          key={product.category} />
      );
    }
    rows.push(
      <ProductRow
        product={product}
        key={product.name} />
    );
    lastCategory = product.category;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

function SearchBar({ filterText, inStockOnly }) {
  return (
    <form>
      <input 
        type="text" 
        value={filterText} 
        placeholder="Search..."/>
      <label>
        <input 
          type="checkbox" 
          checked={inStockOnly} />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

Perhatikan bahwa pengeditan formulir belum berhasil. Ada galat di konsol di sandbox di atas yang menjelaskan alasannya:

Console
You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field.

Pada sandbox di atas, ProductTable dan SearchBar membaca props filterText dan inStockOnly untuk merender tabel, input, dan kotak centang. Sebagai contoh, berikut ini cara SearchBar mengisi nilai input:

function SearchBar({ filterText, inStockOnly }) {
return (
<form>
<input
type="text"
value={filterText}
placeholder="Search..."/>

Namun, Anda belum menambahkan kode apa pun untuk merespons tindakan pengguna seperti mengetik. Ini akan menjadi langkah terakhir Anda.

Step 5: Add inverse data flow

Currently your app renders correctly with props and state flowing down the hierarchy. But to change the state according to user input, you will need to support data flowing the other way: the form components deep in the hierarchy need to update the state in FilterableProductTable.

React makes this data flow explicit, but it requires a little more typing than two-way data binding. If you try to type or check the box in the example above, you’ll see that React ignores your input. This is intentional. By writing <input value={filterText} />, you’ve set the value prop of the input to always be equal to the filterText state passed in from FilterableProductTable. Since filterText state is never set, the input never changes.

You want to make it so whenever the user changes the form inputs, the state updates to reflect those changes. The state is owned by FilterableProductTable, so only it can call setFilterText and setInStockOnly. To let SearchBar update the FilterableProductTable’s state, you need to pass these functions down to SearchBar:

function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);

return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly} />

Inside the SearchBar, you will add the onChange event handlers and set the parent state from them:

<input
type="text"
value={filterText}
placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)} />

Now the application fully works!

import { useState } from 'react';

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

  return (
    <div>
      <SearchBar 
        filterText={filterText} 
        inStockOnly={inStockOnly} 
        onFilterTextChange={setFilterText} 
        onInStockOnlyChange={setInStockOnly} />
      <ProductTable 
        products={products} 
        filterText={filterText}
        inStockOnly={inStockOnly} />
    </div>
  );
}

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

function ProductTable({ products, filterText, inStockOnly }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (
      product.name.toLowerCase().indexOf(
        filterText.toLowerCase()
      ) === -1
    ) {
      return;
    }
    if (inStockOnly && !product.stocked) {
      return;
    }
    if (product.category !== lastCategory) {
      rows.push(
        <ProductCategoryRow
          category={product.category}
          key={product.category} />
      );
    }
    rows.push(
      <ProductRow
        product={product}
        key={product.name} />
    );
    lastCategory = product.category;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

function SearchBar({
  filterText,
  inStockOnly,
  onFilterTextChange,
  onInStockOnlyChange
}) {
  return (
    <form>
      <input 
        type="text" 
        value={filterText} placeholder="Search..." 
        onChange={(e) => onFilterTextChange(e.target.value)} />
      <label>
        <input 
          type="checkbox" 
          checked={inStockOnly} 
          onChange={(e) => onInStockOnlyChange(e.target.checked)} />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

You can learn all about handling events and updating state in the Adding Interactivity section.

Where to go from here

This was a very brief introduction to how to think about building components and applications with React. You can start a React project right now or dive deeper on all the syntax used in this tutorial.