Memilih Struktur State

Menata struktur state dengan baik dapat membuat perbedaan antara komponen yang mudah dimodifikasi dan di-debug, dan komponen yang menjadi sumber kesalahan yang konstan. Berikut adalah beberapa tips yang harus Anda pertimbangkan saat menata struktur state.

You will learn

  • Kapan harus menggunakan satu variabel state vs beberapa variabel state
  • Apa yang harus dihindari ketika mengatur state
  • Bagaimana cara mengatasi masalah umum dengan struktur state

Prinsip-prinsip untuk menata state

Ketika Anda menulis komponen yang memegang beberapa state, Anda harus membuat pilihan tentang berapa banyak variabel state yang harus digunakan dan bagaimana bentuk datanya. Meskipun mungkin saja menulis program yang benar dengan struktur state yang kurang optimal, ada beberapa prinsip yang dapat membimbing Anda untuk membuat pilihan yang lebih baik:

  1. Kelompokkan state yang terkait. Jika Anda selalu memperbarui dua atau lebih variabel state secara bersamaan, pertimbangkan untuk menggabungkannya menjadi satu variabel state tunggal.
  2. Hindari kontradiksi dalam state. Saat state diorganisir sedemikian rupa sehingga beberapa bagian state dapat saling bertentangan dan “tidak sependapat” satu sama lain, maka ini bisa meninggalkan celah untuk kesalahan. Coba hindari hal ini.
  3. Hindari state yang redundan. Jika Anda dapat menghitung beberapa informasi dari prop komponen atau variabel state yang sudah ada selama rendering, maka Anda tidak perlu memasukkan informasi tersebut ke dalam state komponen tersebut.
  4. Hindari duplikasi dalam state. Ketika data yang sama terduplikasi antara beberapa variabel state, atau dalam objek bertingkat, maka akan sulit menjaga sinkronisasi antara mereka. Kurangi duplikasi ketika memungkinkan.
  5. Hindari state yang sangat bertingkat. State dengan hierarkis yang dalam sangat tidak mudah untuk diperbarui. Saat memungkinkan, lebih baik menata state dengan datar.

Tujuan di balik prinsip-prinsip ini adalah membuat state mudah diperbarui tanpa memperkenalkan kesalahan. Menghapus data yang redundan dan duplikat dari state membantu memastikan bahwa semua bagian state tetap sinkron. Ini mirip dengan bagaimana seorang insinyur database mungkin ingin menormalisasi struktur database untuk mengurangi kemungkinan bug. Untuk mem-parafrase Albert Einstein, “Jadikan state Anda sesederhana mungkin - tetapi jangan terlalu sederhana.”

Sekarang mari kita lihat bagaimana prinsip-prinsip tersebut diterapkan dalam tindakan.

Anda mungkin kadang-kadang tidak yakin antara menggunakan satu variabel state atau beberapa variabel state.

Haruskah Anda melakukan hal ini?

const [x, setX] = useState(0);
const [y, setY] = useState(0);

atau ini?

const [position, setPosition] = useState({ x: 0, y: 0 });

Teknisnya, Anda dapat menggunakan kedua pendekatan ini. Namun, jika dua variabel state selalu berubah bersama-sama, mungkin ide yang baik untuk menggabungkannya menjadi satu variabel state. Dengan begitu, Anda tidak akan lupa untuk selalu menjaga keduanya selalu sinkron, seperti dalam contoh ini di mana menggerakkan kursor memperbarui kedua koordinat titik merah:

import { useState } from 'react';

export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: e.clientX,
          y: e.clientY
        });
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  )
}

Ada kasus lain di mana Anda akan mengelompokkan data ke dalam objek atau senarai ketika Anda tidak tahu berapa banyak bagian dari state yang Anda butuhkan. Sebagai contoh, ini berguna ketika Anda memiliki formulir di mana pengguna dapat menambahkan bidang kustom.

Pitfall

Jika variable state Anda adalah sebuah objek, ingatlah bahwa Anda tidak dapat memperbarui hanya satu bidang di dalamnya tanpa menyalin secara eksplisit bidang lainnya. Misalnya, Anda tidak dapat melakukan setPosition({ x: 100 }) pada contoh di atas karena tidak akan memiliki properti y sama sekali! Sebagai gantinya, jika Anda ingin mengatur x saja, Anda akan melakukan setPosition({ ...position, x: 100 }), atau membaginya menjadi dua variabel state dan lakukan setX(100).

Hindari kontradiksi dalam state

Berikut adalah formulir umpan balik hotel dengan variabel state isSending dan isSent:

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    setIsSending(false);
    setIsSent(true);
  }

  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Saat kode ini dijalankan, kode masih memungkinkan terjadinya keadaan “tidak mungkin”. Contohnya, jika kita lupa memanggil setIsSent dan setIsSending bersama-sama, kita dapat berakhir dalam situasi di mana kedua isSending dan isSent bernilai true pada saat yang sama. Semakin kompleks komponen Anda, semakin sulit untuk memahami apa yang terjadi

Sejak isSending dan isSent seharusnya tidak pernah bernilai true pada saat yang sama, lebih baik menggantinya dengan satu variabel state status yang dapat mengambil salah satu dari tiga status yang valid: 'typing' (initial), 'sending', and 'sent':

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    await sendMessage(text);
    setStatus('sent');
  }

  const isSending = status === 'sending';
  const isSent = status === 'sent';

  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Berpura-puralah mengirim pesan.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Anda masih bisa mendeklarasikan beberapa konstanta untuk keterbacaan:

const isSending = status === 'sending';
const isSent = status === 'sent';

Tetapi itu bukan variabel state, jadi Anda tidak perlu khawatir tentang kesalahan sinkronisasi antar variabel.

Hindari state yang redundan

Jika Anda dapat menghitung beberapa informasi dari prop komponen atau variabel state yang sudah ada selama me-render, Anda tidak harus meletakkan informasi tersebut ke dalam state komponen tersebut.

Sebagai contoh, ambil formulir ini. Ini berfungsi, tetapi dapatkah Anda menemukan keadaan yang redundan di dalamnya?

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

Formulir ini mempunya 3 variabel state: firstName, lastName, dan fullName. Namun, fullName adalah redundan. Y Anda selalu dapat menghitung fullName dari firstName dan lastName selama render, sehingga hapus dari keadaan.

Ini adalah bagaimana Anda dapat melakukannya:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

Di sini, fullName bukan merupakan sebuah variabel state. Sebaliknya, nilai fullName dihitung saat render:

const fullName = firstName + ' ' + lastName;

Sebagai hasilnya, pengontrol perubahan tidak perlu melakukan apa pun khusus untuk memperbarui fullName. Ketika Anda memanggil setFirstName atau setLastName, Anda memicu render ulang, dan kemudian fullName berikutnya akan dihitung dari data terbaru.

Deep Dive

Jangan meniru props di dalam state

Contoh umum code yang memiliki state yang redundan seperti dibawah ini:

function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);

Di sini, sebuah variabel state color diinisialisasi dengan prop messageColor. Masalahnya adalah bahwa jika komponen induk memberikan nilai messageColor berbeda (misalnya, 'red' daripada 'blue'), variabel state color tidak akan diperbarui! State hanya di-inisialisasi selama render pertama.

Ini mengapa “menirukan” beberapa prop pada variabel state dapat menyebabkan kebingungan. Sebaliknya, gunakan prop messageColor langsung dalam kode Anda. Jika Anda ingin memberinya nama yang lebih pendek, gunakan konstanta:

function Message({ messageColor }) {
const color = messageColor;

Dengan cara ini, state tidak akan keluar dari sinkron dengan prop yang dilewatkan dari komponen induk.

“Menggandakan” props ke dalam state hanya masuk akal ketika Anda ingin mengabaikan semua pembaruan untuk prop tertentu. Secara konvensional, awali nama prop dengan initial atau default untuk menjelaskan bahwa nilai baru prop tersebut diabaikan:

function Message({ initialColor }) {
// Variabel state `color` menyimpan nilai pertama kali dari `initialColor`.
// Perubahan lebih lanjut pada prop `initialColor` diabaikan.
const [color, setColor] = useState(initialColor);

Hindari duplikasi dalam state

Komponen daftar menu ini memungkinkan Anda memilih satu camilan dari beberapa pilihan:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

Saat ini, komponen daftar menu ini menyimpan item yang dipilih sebagai objek dalam variabel state selectedItem. Namun, hal ini tidak bagus: isi selectedItem adalah objek yang sama dengan salah satu item dalam daftar items. Ini berarti informasi tentang item itu sendiri diduplikasi di dua tempat.

Mengapa ini menjadi masalah? Mari kita buat setiap item dapat diedit:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2> 
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

Perhatikan bahwa jika Anda pertama kali mengklik “Pilih” pada item dan kemudian mengeditnya, input akan diperbarui tetapi label di bagian bawah tidak mencerminkan suntingan tersebut. Hal ini terjadi karena adanya duplikasi state, dan kamu lupa untuk memperbarui selectedItem.

Meskipun Anda bisa memperbarui selectedItem juga, perbaikan yang lebih mudah adalah menghilangkan duplikasi. Pada contoh ini, daripada menggunakan objek selectedItem (yang menciptakan duplikasi dengan objek yang ada di dalam items), Anda menyimpan selectedId di dalam state, dan kemudian mendapatkan selectedItem dengan mencari senarai items untuk item dengan ID tersebut:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item =>
    item.id === selectedId
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedId(item.id);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

(Sebagai alternatif, Anda dapat menyimpan indeks yang dipilih di dalam state.)

State sebelumnya diduplikasi seperti ini:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

Tetapi setelah diubah menjadi seperti ini:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

Duplikasi data sudah tidak ada lagi, dan hanya menyimpan state yang penting!

Sekarang jika Anda mengubah item yang dipilih, pesan di bawahnya akan segera diperbarui. Ini karena setItems memicu render ulang, dan items.find(...) akan menemukan item dengan judul yang diperbarui. Anda tidak perlu menyimpan item yang dipilih di state, karena hanya ID yang dipilih yang penting. Yang lain dapat dihitung selama render.

Hindari state yang sangat bertingkat

Bayangkan rencana perjalanan yang terdiri dari planet, benua, dan negara. Anda mungkin tergoda untuk mengatur state-nya menggunakan objek dan senarai yang bersarang, seperti contoh ini:

export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Earth',
    childPlaces: [{
      id: 2,
      title: 'Africa',
      childPlaces: [{
        id: 3,
        title: 'Botswana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egypt',
        childPlaces: []
      }, {
        id: 5,
        title: 'Kenya',
        childPlaces: []
      }, {
        id: 6,
        title: 'Madagascar',
        childPlaces: []
      }, {
        id: 7,
        title: 'Morocco',
        childPlaces: []
      }, {
        id: 8,
        title: 'Nigeria',
        childPlaces: []
      }, {
        id: 9,
        title: 'South Africa',
        childPlaces: []
      }]
    }, {
      id: 10,
      title: 'Americas',
      childPlaces: [{
        id: 11,
        title: 'Argentina',
        childPlaces: []
      }, {
        id: 12,
        title: 'Brazil',
        childPlaces: []
      }, {
        id: 13,
        title: 'Barbados',
        childPlaces: []
      }, {
        id: 14,
        title: 'Canada',
        childPlaces: []
      }, {
        id: 15,
        title: 'Jamaica',
        childPlaces: []
      }, {
        id: 16,
        title: 'Mexico',
        childPlaces: []
      }, {
        id: 17,
        title: 'Trinidad and Tobago',
        childPlaces: []
      }, {
        id: 18,
        title: 'Venezuela',
        childPlaces: []
      }]
    }, {
      id: 19,
      title: 'Asia',
      childPlaces: [{
        id: 20,
        title: 'China',
        childPlaces: []
      }, {
        id: 21,
        title: 'Hong Kong',
        childPlaces: []
      }, {
        id: 22,
        title: 'India',
        childPlaces: []
      }, {
        id: 23,
        title: 'Singapore',
        childPlaces: []
      }, {
        id: 24,
        title: 'South Korea',
        childPlaces: []
      }, {
        id: 25,
        title: 'Thailand',
        childPlaces: []
      }, {
        id: 26,
        title: 'Vietnam',
        childPlaces: []
      }]
    }, {
      id: 27,
      title: 'Europe',
      childPlaces: [{
        id: 28,
        title: 'Croatia',
        childPlaces: [],
      }, {
        id: 29,
        title: 'France',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Germany',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Italy',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Spain',
        childPlaces: [],
      }, {
        id: 34,
        title: 'Turkey',
        childPlaces: [],
      }]
    }, {
      id: 35,
      title: 'Oceania',
      childPlaces: [{
        id: 36,
        title: 'Australia',
        childPlaces: [],
      }, {
        id: 37,
        title: 'Bora Bora (French Polynesia)',
        childPlaces: [],
      }, {
        id: 38,
        title: 'Easter Island (Chile)',
        childPlaces: [],
      }, {
        id: 39,
        title: 'Fiji',
        childPlaces: [],
      }, {
        id: 40,
        title: 'Hawaii (the USA)',
        childPlaces: [],
      }, {
        id: 41,
        title: 'New Zealand',
        childPlaces: [],
      }, {
        id: 42,
        title: 'Vanuatu',
        childPlaces: [],
      }]
    }]
  }, {
    id: 43,
    title: 'Moon',
    childPlaces: [{
      id: 44,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 45,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 46,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 47,
    title: 'Mars',
    childPlaces: [{
      id: 48,
      title: 'Corn Town',
      childPlaces: []
    }, {
      id: 49,
      title: 'Green Hill',
      childPlaces: []      
    }]
  }]
};

Sekarang katakanlah Anda ingin menambahkan tombol untuk menghapus tempat yang telah Anda kunjungi. Bagaimana cara melakukannya? Memperbarui state yang bertingkat melibatkan membuat salinan objek sepanjang jalan dari bagian yang berubah. Menghapus tempat yang sangat tertanam akan melibatkan menyalin seluruh rantai tempat induknya. Kode semacam itu bisa sangat panjang.

Jika state terlalu bersarang untuk diperbarui dengan mudah, pertimbangkan untuk membuatnya “datar”. Berikut adalah salah satu cara Anda dapat memperbarui struktur data ini. Alih-alih struktur seperti pohon di mana setiap tempat memiliki sebuah senarai dari tempat anaknya, Anda dapat membuat setiap tempat memegang sebuah senarai dari ID tempat anaknya. Kemudian simpan pemetaan dari setiap ID tempat ke tempat yang sesuai.

Penataan data ini mungkin mengingatkan Anda pada tabel di basis data:

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 43, 47],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 27, 35]
  },
  2: {
    id: 2,
    title: 'Africa',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  }, 
  3: {
    id: 3,
    title: 'Botswana',
    childIds: []
  },
  4: {
    id: 4,
    title: 'Egypt',
    childIds: []
  },
  5: {
    id: 5,
    title: 'Kenya',
    childIds: []
  },
  6: {
    id: 6,
    title: 'Madagascar',
    childIds: []
  }, 
  7: {
    id: 7,
    title: 'Morocco',
    childIds: []
  },
  8: {
    id: 8,
    title: 'Nigeria',
    childIds: []
  },
  9: {
    id: 9,
    title: 'South Africa',
    childIds: []
  },
  10: {
    id: 10,
    title: 'Americas',
    childIds: [11, 12, 13, 14, 15, 16, 17, 18],   
  },
  11: {
    id: 11,
    title: 'Argentina',
    childIds: []
  },
  12: {
    id: 12,
    title: 'Brazil',
    childIds: []
  },
  13: {
    id: 13,
    title: 'Barbados',
    childIds: []
  }, 
  14: {
    id: 14,
    title: 'Canada',
    childIds: []
  },
  15: {
    id: 15,
    title: 'Jamaica',
    childIds: []
  },
  16: {
    id: 16,
    title: 'Mexico',
    childIds: []
  },
  17: {
    id: 17,
    title: 'Trinidad and Tobago',
    childIds: []
  },
  18: {
    id: 18,
    title: 'Venezuela',
    childIds: []
  },
  19: {
    id: 19,
    title: 'Asia',
    childIds: [20, 21, 22, 23, 24, 25, 26],   
  },
  20: {
    id: 20,
    title: 'China',
    childIds: []
  },
  21: {
    id: 21,
    title: 'Hong Kong',
    childIds: []
  },
  22: {
    id: 22,
    title: 'India',
    childIds: []
  },
  23: {
    id: 23,
    title: 'Singapore',
    childIds: []
  },
  24: {
    id: 24,
    title: 'South Korea',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Thailand',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Vietnam',
    childIds: []
  },
  27: {
    id: 27,
    title: 'Europe',
    childIds: [28, 29, 30, 31, 32, 33, 34],   
  },
  28: {
    id: 28,
    title: 'Croatia',
    childIds: []
  },
  29: {
    id: 29,
    title: 'France',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Germany',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Italy',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Portugal',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Spain',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Turkey',
    childIds: []
  },
  35: {
    id: 35,
    title: 'Oceania',
    childIds: [36, 37, 38, 39, 40, 41, 42],   
  },
  36: {
    id: 36,
    title: 'Australia',
    childIds: []
  },
  37: {
    id: 37,
    title: 'Bora Bora (French Polynesia)',
    childIds: []
  },
  38: {
    id: 38,
    title: 'Easter Island (Chile)',
    childIds: []
  },
  39: {
    id: 39,
    title: 'Fiji',
    childIds: []
  },
  40: {
    id: 40,
    title: 'Hawaii (the USA)',
    childIds: []
  },
  41: {
    id: 41,
    title: 'New Zealand',
    childIds: []
  },
  42: {
    id: 42,
    title: 'Vanuatu',
    childIds: []
  },
  43: {
    id: 43,
    title: 'Moon',
    childIds: [44, 45, 46]
  },
  44: {
    id: 44,
    title: 'Rheita',
    childIds: []
  },
  45: {
    id: 45,
    title: 'Piccolomini',
    childIds: []
  },
  46: {
    id: 46,
    title: 'Tycho',
    childIds: []
  },
  47: {
    id: 47,
    title: 'Mars',
    childIds: [48, 49]
  },
  48: {
    id: 48,
    title: 'Corn Town',
    childIds: []
  },
  49: {
    id: 49,
    title: 'Green Hill',
    childIds: []
  }
};

Sekarang karena state-nya “datar” (juga dikenal sebagai “dinormalisasi”), memperbarui item yang bersarang menjadi lebih mudah.

Untuk menghapus sebuah tempat sekarang, Anda hanya perlu memperbarui dua level state:

  • Versi terbaru dari parent tempatnya harus menghapus ID yang dihapus dari senarai childIds.
  • Versi terbaru dari objek ”table” induk harus mencakup versi terbaru dari tempat parentnya.

Berikut adalah contoh bagaimana Anda bisa melakukannya:

import { useState } from 'react';
import { initialTravelPlan } from './places.js';

export default function TravelPlan() {
  const [plan, setPlan] = useState(initialTravelPlan);

  function handleComplete(parentId, childId) {
    const parent = plan[parentId];
    // Buatlah versi baru dari induk tempat tersebut
    // Buatlah versi baru dari parent place yang tidak termasuk ID child ini
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    };
    // Perbarui objek state root...
    setPlan({
      ...plan,
      // ...Perbarui objek state induk sehingga memiliki parent yang telah diperbarui.
      [parentId]: nextParent
    });
  }

  const root = plan[0];
  const planetIds = root.childIds;
  return (
    <>
      <h2>Places to visit</h2>
      <ol>
        {planetIds.map(id => (
          <PlaceTree
            key={id}
            id={id}
            parentId={0}
            placesById={plan}
            onComplete={handleComplete}
          />
        ))}
      </ol>
    </>
  );
}

function PlaceTree({ id, parentId, placesById, onComplete }) {
  const place = placesById[id];
  const childIds = place.childIds;
  return (
    <li>
      {place.title}
      <button onClick={() => {
        onComplete(parentId, id);
      }}>
        Complete
      </button>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      }
    </li>
  );
}

Anda dapat menempatkan state sebanyak yang Anda inginkan, tetapi membuatnya menjadi “datar” dapat memecahkan banyak masalah. Ini membuat state lebih mudah diperbarui, dan membantu memastikan Anda tidak memiliki duplikasi di bagian yang berbeda dari objek bertingkat

Deep Dive

Meningkatkan penggunaan memori

Idealnya, Anda juga harus menghapus item yang dihapus (dan anak-anaknya!) dari objek “table” untuk meningkatkan penggunaan memori. Versi ini melakukan itu. ini juga menggunakan Immer untuk membuat logika pembaruan lebih ringkas.

import { useImmer } from 'use-immer';
import { initialTravelPlan } from './places.js';

export default function TravelPlan() {
  const [plan, updatePlan] = useImmer(initialTravelPlan);

  function handleComplete(parentId, childId) {
    updatePlan(draft => {
      // Hapus ID anak dari tempat induknya.
      const parent = draft[parentId];
      parent.childIds = parent.childIds
        .filter(id => id !== childId);

      // Melupakan tempat ini dan semua subtree-nya.
      deleteAllChildren(childId);
      function deleteAllChildren(id) {
        const place = draft[id];
        place.childIds.forEach(deleteAllChildren);
        delete draft[id];
      }
    });
  }

  const root = plan[0];
  const planetIds = root.childIds;
  return (
    <>
      <h2>Places to visit</h2>
      <ol>
        {planetIds.map(id => (
          <PlaceTree
            key={id}
            id={id}
            parentId={0}
            placesById={plan}
            onComplete={handleComplete}
          />
        ))}
      </ol>
    </>
  );
}

function PlaceTree({ id, parentId, placesById, onComplete }) {
  const place = placesById[id];
  const childIds = place.childIds;
  return (
    <li>
      {place.title}
      <button onClick={() => {
        onComplete(parentId, id);
      }}>
        Complete
      </button>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      }
    </li>
  );
}

Kadang-kadang, Anda juga dapat mengurangi penempelan status dengan memindahkan beberapa penempelan status ke komponen anak. Ini bekerja dengan baik untuk status UI sementara yang tidak perlu disimpan, seperti apakah sebuah item di-hover.

Recap

  • Jika dua variabel state selalu diperbarui bersama, pertimbangkan untuk menggabungkannya menjadi satu.
  • Pilih variabel state dengan hati-hati untuk menghindari menciptakan keadaan yang “mustahil”.
  • Strukturkan state Anda sedemikian rupa sehingga mengurangi kemungkinan kesalahan saat memperbarui state.
  • Hindari penggunaan state yang redundan dan duplikat sehingga tidak perlu menjaga sinkronisasi.
  • Jangan memasukkan props ke dalam state kecuali Anda secara khusus ingin mencegah pembaruan.
  • Untuk pola UI seperti pemilihan, simpan ID atau indeks dalam state daripada objek itu sendiri.
  • Jika memperbarui state yang sangat berlapis-lapis menjadi rumit, coba datanya didatarkan.

Challenge 1 of 4:
Sesuaikan komponen yang tidak terbarui

Komponen Clock ini menerima dua prop: color dan time. Ketika Anda memilih warna yang berbeda pada kotak pilihan, Clock menerima prop color yang berbeda dari komponen induknya. Namun, warna yang ditampilkan tidak diperbarui. Mengapa? Perbaiki masalahnya.

import { useState } from 'react';

export default function Clock(props) {
  const [color, setColor] = useState(props.color);
  return (
    <h1 style={{ color: color }}>
      {props.time}
    </h1>
  );
}