Mempertahankan dan Mengatur Ulang State

State diisolasi antar komponen. React melacak state mana yang dimiliki oleh komponen mana berdasarkan tempatnya di pohon antarmuka pengguna (UI). Anda dapat mengontrol kapan harus mempertahankan state dan kapan harus mengatur ulang di antara render ulang (re-render).

You will learn

  • Bagaimana React “melihat” struktur komponen
  • Kapan React memilih untuk mempertahankan atau mengatur ulang state
  • Bagaimana cara memaksa React untuk mengatur ulang state komponen
  • Bagaimana keys dan types mempengaruhi apakah state dipertahankan

Pohon antarmuka pengguna (UI)

Peramban menggunakan banyak struktur pohon untuk memodelkan antarmuka pengguna (UI). DOM mewakili elemen HTML, CSSOM melakukan hal yang sama untuk CSS. Bahkan ada Pohon aksesibilitas!

React juga menggunakan struktur pohon untuk mengelola dan memodelkan UI yang Anda buat. React membuat pohon UI dari JSX Anda. Kemudian React DOM memperbarui elemen-elemen DOM peramban agar sesuai dengan pohon UI tersebut (React Native menerjemahkan pohon-pohon tersebut menjadi elemen-elemen yang spesifik untuk platform mobile).

Diagram with three sections arranged horizontally. In the first section, there are three rectangles stacked vertically, with labels 'Component A', 'Component B', and 'Component C'. Transitioning to the next pane is an arrow with the React logo on top labeled 'React'. The middle section contains a tree of components, with the root labeled 'A' and two children labeled 'B' and 'C'. The next section is again transitioned using an arrow with the React logo on top labeled 'React'. The third and final section is a wireframe of a browser, containing a tree of 8 nodes, which has only a subset highlighted (indicating the subtree from the middle section).
Diagram with three sections arranged horizontally. In the first section, there are three rectangles stacked vertically, with labels 'Component A', 'Component B', and 'Component C'. Transitioning to the next pane is an arrow with the React logo on top labeled 'React'. The middle section contains a tree of components, with the root labeled 'A' and two children labeled 'B' and 'C'. The next section is again transitioned using an arrow with the React logo on top labeled 'React'. The third and final section is a wireframe of a browser, containing a tree of 8 nodes, which has only a subset highlighted (indicating the subtree from the middle section).

Dari komponen, React membuat pohon UI yang digunakan React DOM untuk merender DOM

State terikat dengan posisi di dalam pohon

Ketika Anda memberikan state pada sebuah komponen, Anda mungkin berpikir bahwa state tersebut “hidup” di dalam komponen. Tetapi state sebenarnya disimpan di dalam React. React mengasosiasikan setiap bagian dari state yang dipegangnya dengan komponen yang benar berdasarkan posisi komponen tersebut di dalam pohon UI.

Di sini, hanya ada satu tag JSX <Counter />, tetapi tag tersebut dirender pada dua posisi yang berbeda:

import { useState } from 'react';

export default function App() {
  const counter = <Counter />;
  return (
    <div>
      {counter}
      {counter}
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Beginilah tampilannya sebagai pohon:

Diagram of a tree of React components. The root node is labeled 'div' and has two children. Each of the children are labeled 'Counter' and both contain a state bubble labeled 'count' with value 0.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. Each of the children are labeled 'Counter' and both contain a state bubble labeled 'count' with value 0.

Pohon React

Ini adalah dua penghitung yang terpisah karena masing-masing di-render pada posisinya sendiri di dalam pohon. Anda biasanya tidak perlu memikirkan posisi-posisi ini untuk menggunakan React, tetapi akan sangat berguna untuk memahami cara kerjanya.

Dalam React, setiap komponen pada layar memiliki state yang terisolasi sepenuhnya. Sebagai contoh, jika Anda me-render dua komponen Counter secara berdampingan, masing-masing komponen akan mendapatkan state-nya sendiri-sendiri, independen, yaitu state score dan hover.

Coba klik kedua penghitung dan perhatikan bahwa keduanya tidak saling mempengaruhi:

import { useState } from 'react';

export default function App() {
  return (
    <div>
      <Counter />
      <Counter />
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Seperti yang dapat Anda lihat, ketika satu penghitung diperbarui, hanya state untuk komponen tersebut yang diperbarui:

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 1. The state bubble of the right child is highlighted in yellow to indicate its value has updated.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 1. The state bubble of the right child is highlighted in yellow to indicate its value has updated.

Memperbarui state

React akan mempertahankan state selama Anda me-render komponen yang sama pada posisi yang sama. Untuk melihat hal ini, naikkan kedua penghitung, lalu hapus komponen kedua dengan menghapus centang pada checkbox “Render the second counter”, lalu tambahkan kembali dengan mencentangnya lagi:

import { useState } from 'react';

export default function App() {
  const [showB, setShowB] = useState(true);
  return (
    <div>
      <Counter />
      {showB && <Counter />} 
      <label>
        <input
          type="checkbox"
          checked={showB}
          onChange={e => {
            setShowB(e.target.checked)
          }}
        />
        Render the second counter
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Perhatikan bagaimana saat Anda berhenti me-render penghitung kedua, state-nya akan hilang sepenuhnya. Hal ini dikarenakan ketika React menghapus sebuah komponen, ia akan menghancurkan state-nya.

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is missing, and in its place is a yellow 'poof' image, highlighting the component being deleted from the tree.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is missing, and in its place is a yellow 'poof' image, highlighting the component being deleted from the tree.

Menghapus komponen

Ketika Anda mencentang “Render the second counter”, Counter kedua dan state-nya diinisialisasi dari awal (score = 0) dan ditambahkan ke DOM.

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The entire right child node is highlighted in yellow, indicating that it was just added to the tree.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The entire right child node is highlighted in yellow, indicating that it was just added to the tree.

Menambahkan komponen

React mempertahankan state sebuah komponen selama komponen tersebut di-render pada posisinya di pohon UI. Jika komponen tersebut dihapus, atau komponen lain di-render pada posisi yang sama, React akan membuang state-nya.

Komponen yang sama pada posisi yang sama mempertahankan state

Pada contoh ini, terdapat dua tag <Counter /> yang berbeda:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Ketika Anda mencentang atau menghapus checkbox, state penghitung tidak diatur ulang. Entah isFancy bernilai true atau false, Anda selalu memiliki <Counter /> sebagai anak pertama dari div yang dikembalikan dari komponen akar App:

Diagram with two sections separated by an arrow transitioning between them. Each section contains a layout of components with a parent labeled 'App' containing a state bubble labeled isFancy. This component has one child labeled 'div', which leads to a prop bubble containing isFancy (highlighted in purple) passed down to the only child. The last child is labeled 'Counter' and contains a state bubble with label 'count' and value 3 in both diagrams. In the left section of the diagram, nothing is highlighted and the isFancy parent state value is false. In the right section of the diagram, the isFancy parent state value has changed to true and it is highlighted in yellow, and so is the props bubble below, which has also changed its isFancy value to true.
Diagram with two sections separated by an arrow transitioning between them. Each section contains a layout of components with a parent labeled 'App' containing a state bubble labeled isFancy. This component has one child labeled 'div', which leads to a prop bubble containing isFancy (highlighted in purple) passed down to the only child. The last child is labeled 'Counter' and contains a state bubble with label 'count' and value 3 in both diagrams. In the left section of the diagram, nothing is highlighted and the isFancy parent state value is false. In the right section of the diagram, the isFancy parent state value has changed to true and it is highlighted in yellow, and so is the props bubble below, which has also changed its isFancy value to true.

Memperbarui state App tidak mengatur ulang Counter karena Counter tetap berada di posisi yang sama

Ini adalah komponen yang sama pada posisi yang sama, jadi dari sudut pandang React, ini adalah penghitung yang sama.

Pitfall

Ingatlah bahwa posisi pada pohon UI—bukan pada markup JSX—yang penting pada React! Komponen ini memiliki dua klausa return dengan tag JSX <Counter /> yang berbeda di dalam dan di luar if:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  if (isFancy) {
    return (
      <div>
        <Counter isFancy={true} />
        <label>
          <input
            type="checkbox"
            checked={isFancy}
            onChange={e => {
              setIsFancy(e.target.checked)
            }}
          />
          Use fancy styling
        </label>
      </div>
    );
  }
  return (
    <div>
      <Counter isFancy={false} />
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Anda mungkin berharap state akan diatur ulang ketika Anda mencentang checkbox, tetapi ternyata tidak! Hal ini dikarenakan kedua tag <Counter /> di-render pada posisi yang sama. React tidak mengetahui di mana Anda meletakkan kondisi di dalam fungsi Anda. Yang ia “lihat” hanyalah pohon yang Anda kembalikan.

Pada kedua kasus tersebut, komponen App mengembalikan <div> dengan <Counter /> sebagai anak pertama. Bagi React, kedua penghitung ini memiliki “alamat” yang sama: anak pertama dari anak pertama dari akar. Ini adalah cara React mencocokkan keduanya antara render sebelumnya dan berikutnya, terlepas dari bagaimana Anda menyusun logika Anda.

Komponen yang berbeda pada posisi state reset yang sama

Pada contoh ini, mencentang checkbox akan menggantikan <Counter> dengan <p>:

import { useState } from 'react';

export default function App() {
  const [isPaused, setIsPaused] = useState(false);
  return (
    <div>
      {isPaused ? (
        <p>See you later!</p> 
      ) : (
        <Counter /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isPaused}
          onChange={e => {
            setIsPaused(e.target.checked)
          }}
        />
        Take a break
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Di sini, Anda beralih di antara jenis komponen yang berbeda pada posisi yang sama. Awalnya, anak pertama dari <div> berisi sebuah Counter. Namun ketika Anda menukar p, React menghapus Counter dari pohon UI dan menghancurkan state-nya.

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'p', highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'p', highlighted in yellow.

Ketika Counter berubah menjadi p, Counter dihapus dan p ditambahkan

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'p'. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'p'. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, highlighted in yellow.

Saat beralih kembali, p dihapus dan Counter ditambahkan

Selain itu, ketika Anda merender komponen yang berbeda pada posisi yang sama, komponen tersebut akan mengatur ulang state seluruh subpohonnya. Untuk melihat cara kerjanya, tingkatkan penghitungnya, lalu centang checkbox:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <div>
          <Counter isFancy={true} /> 
        </div>
      ) : (
        <section>
          <Counter isFancy={false} />
        </section>
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

State penghitung akan diatur ulang saat Anda mengklik checkbox. Meskipun Anda me-render Counter, anak pertama dari div berubah dari div menjadi section. Ketika anak div dihapus dari DOM, seluruh pohon di bawahnya (termasuk Counter dan state-nya) juga dihancurkan.

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'section', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'div', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'section', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'div', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.

Ketika section berubah menjadi div, section akan dihapus dan div yang baru ditambahkan

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'div', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 0. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'section', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'div', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 0. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'section', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.

Saat beralih kembali, div akan dihapus dan section yang baru ditambahkan

Sebagai aturan praktis, jika Anda ingin mempertahankan state di antara render ulang, struktur pohon Anda harus “cocok” dari satu render ke render lainnya. Jika strukturnya berbeda, state akan dihancurkan karena React menghancurkan state ketika menghapus sebuah komponen dari pohon.

Pitfall

Inilah alasan mengapa Anda tidak boleh menyarangkan definisi fungsi komponen.

Di sini, fungsi komponen MyTextField didefinisikan di dalam MyComponent:

import { useState } from 'react';

export default function MyComponent() {
  const [counter, setCounter] = useState(0);

  function MyTextField() {
    const [text, setText] = useState('');

    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
  }

  return (
    <>
      <MyTextField />
      <button onClick={() => {
        setCounter(counter + 1)
      }}>Clicked {counter} times</button>
    </>
  );
}

Setiap kali Anda mengklik tombol, state masukan akan menghilang! Hal ini disebabkan karena fungsi MyTextField yang berbeda dibuat untuk setiap render MyComponent. Anda me-render komponen yang berbeda pada posisi yang sama, sehingga React akan mengatur ulang semua state di bawah ini. Hal ini menyebabkan bug dan masalah performa. Untuk menghindari masalah ini, selalu deklarasikan fungsi komponen pada level teratas, dan jangan menumpuk definisinya..

Mengatur ulang state pada posisi yang sama

Secara default, React mempertahankan state dari sebuah komponen ketika komponen tersebut berada pada posisi yang sama. Biasanya, ini adalah hal yang Anda inginkan, sehingga masuk akal jika ini menjadi perilaku default. Namun terkadang, Anda mungkin ingin mengatur ulang state sebuah komponen. Pertimbangkan aplikasi ini yang memungkinkan dua pemain melacak skor mereka selama setiap giliran:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter person="Taylor" />
      ) : (
        <Counter person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Saat ini, ketika Anda mengganti pemain, skor tetap dipertahankan. Kedua Counter muncul di posisi yang sama, sehingga React melihat mereka sebagai Counter yang sama yang mana props person telah berubah.

Namun secara konseptual, dalam aplikasi ini mereka seharusnya menjadi dua penghitung yang terpisah. Mereka mungkin muncul di tempat yang sama di UI, tetapi yang satu adalah penghitung untuk Taylor, dan yang lainnya adalah penghitung untuk Sarah.

Ada dua opsi untuk mengatur ulang state ketika beralih di antara keduanya:

  1. Merender komponen dalam posisi yang berbeda
  2. Berikan setiap komponen identitas eksplisit dengan key

Opsi 1: Me-render komponen pada posisi yang berbeda

Jika Anda ingin kedua Counter ini independen, Anda dapat membuat mereka dalam dua posisi yang berbeda:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA &&
        <Counter person="Taylor" />
      }
      {!isPlayerA &&
        <Counter person="Sarah" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

  • Awalnya, isPlayerA adalah true. Jadi posisi pertama berisi state Counter, dan posisi kedua kosong.
  • Ketika Anda mengklik tombol “Next player”, posisi pertama akan hilang, namun posisi kedua sekarang berisi Counter.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The only child, arranged to the left, is labeled Counter with a state bubble labeled 'count' and value 0. All of the left child is highlighted in yellow, indicating it was added.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The only child, arranged to the left, is labeled Counter with a state bubble labeled 'count' and value 0. All of the left child is highlighted in yellow, indicating it was added.

State awal

Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'false'. The state bubble is highlighted in yellow, indicating that it has changed. The left child is replaced with a yellow 'poof' image indicating that it has been deleted and there is a new child on the right, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'false'. The state bubble is highlighted in yellow, indicating that it has changed. The left child is replaced with a yellow 'poof' image indicating that it has been deleted and there is a new child on the right, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0.

Mengklik “next”

Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The state bubble is highlighted in yellow, indicating that it has changed. There is a new child on the left, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is replaced with a yellow 'poof' image indicating that it has been deleted.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The state bubble is highlighted in yellow, indicating that it has changed. There is a new child on the left, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is replaced with a yellow 'poof' image indicating that it has been deleted.

Mengklik “next” lagi

Setiap state Counter akan dihancurkan setiap kali dihapus dari DOM. Inilah sebabnya mengapa mereka mengatur ulang setiap kali Anda mengklik tombol.

Solusi ini nyaman ketika Anda hanya memiliki beberapa komponen independen yang di-render di tempat yang sama. Dalam contoh ini, Anda hanya memiliki dua komponen, sehingga tidak merepotkan untuk me-render keduanya secara terpisah di JSX.

Opsi 2: Mengatur ulang state dengan key

Ada juga cara lain yang lebih umum untuk mengatur ulang state komponen.

Anda mungkin pernah melihat key ketika merender list. Key tidak hanya untuk list! Anda dapat menggunakan key untuk membuat React membedakan antar komponen. Secara default, React menggunakan urutan di dalam induk (“penghitung pertama”, “penghitung kedua”) untuk membedakan antar komponen. Tetapi dengan key, Anda dapat memberi tahu React bahwa ini bukan hanya penghitung pertama, atau penghitung kedua, tetapi penghitung yang spesifik—sebagai contoh, penghitung Taylor. Dengan cara ini, React akan mengetahui penghitung Taylor di mana pun dia muncul di dalam pohon!

Pada contoh ini, kedua <Counter /> tidak berbagi state meskipun keduanya muncul di tempat yang sama di JSX:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter key="Taylor" person="Taylor" />
      ) : (
        <Counter key="Sarah" person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Beralih antara Taylor dan Sarah tidak akan mempertahankan state. Ini karena Anda memberi mereka key yang berbeda:

{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}

Menentukan sebuah key memberitahu React untuk menggunakan key itu sendiri sebagai bagian dari posisi, bukan urutan mereka di dalam induk. Inilah sebabnya, meskipun Anda me-render mereka di tempat yang sama di JSX, React melihat mereka sebagai dua penghitung yang berbeda, sehingga mereka tidak akan pernah berbagi state. Setiap kali penghitung muncul di layar, state-nya dibuat. Setiap kali dihapus, state-nya akan dihancurkan. Mengalihkan di antara keduanya akan mengatur ulang state mereka berulang kali.

Note

Ingatlah bahwa key tidak unik secara global. Mereka hanya menentukan posisi dalam induk.

Mengatur ulang formulir dengan tombol

Mengatur ulang state dengan tombol sangat berguna terutama ketika berurusan dengan formulir.

Dalam aplikasi obrolan ini, komponen <Chat> berisi state masukan teks:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

Coba masukkan sesuatu ke dalam input, lalu tekan “Alice” atau “Bob” untuk memilih penerima yang berbeda. Anda akan melihat bahwa state masukan dipertahankan karena <Chat> di-render pada posisi yang sama di pohon.

Di banyak aplikasi, ini mungkin merupakan perilaku yang diinginkan, tetapi tidak di aplikasi obrolan! Anda tidak ingin membiarkan pengguna mengirim pesan yang telah mereka ketik ke orang yang salah karena klik yang tidak disengaja. Untuk memperbaikinya, tambahkan key:

<Chat key={to.id} contact={to} />

Hal ini memastikan bahwa ketika Anda memilih penerima yang berbeda, komponen Chat akan dibuat ulang dari awal, termasuk state apa pun di dalam pohon di bawahnya. React juga akan membuat ulang elemen DOM daripada menggunakannya kembali.

Sekarang, mengganti penerima selalu mengosongkan bidang teks:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.id} contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

Deep Dive

Mempertahankan state untuk komponen yang dilepas

Dalam aplikasi obrolan yang sebenarnya, Anda mungkin ingin memulihkan state masukan ketika pengguna memilih penerima sebelumnya lagi. Ada beberapa cara untuk menjaga state “hidup” untuk komponen yang tidak lagi terlihat:

  • Anda dapat merender semua obrolan, bukan hanya obrolan yang sedang berlangsung, tetapi menyembunyikan semua obrolan lainnya dengan CSS. Obrolan tidak akan dihapus dari pohon, sehingga state lokalnya akan dipertahankan. Solusi ini bekerja dengan baik untuk UI yang sederhana. Tetapi ini bisa menjadi sangat lambat jika pohon yang disembunyikan berukuran besar dan berisi banyak simpul DOM.
  • Anda dapat mengangkat state dan menyimpan pesan yang tertunda untuk setiap penerima di komponen induk. Dengan cara ini, ketika komponen anak dihapus, tidak menjadi masalah, karena induklah yang menyimpan informasi penting. Ini adalah solusi yang paling umum.
  • Anda juga dapat menggunakan sumber yang berbeda selain state React. Sebagai contoh, Anda mungkin ingin draf pesan tetap ada meskipun pengguna tidak sengaja menutup halaman. Untuk mengimplementasikan hal ini, Anda dapat membuat komponen Chat menginisialisasi state-nya dengan membaca dari localStorage, dan menyimpan draft di sana juga.

Apapun strategi yang Anda pilih, obrolan dengan Alice secara konseptual berbeda dengan obrolan dengan Bob, sehingga masuk akal untuk memberikan key pada pohon <Chat> berdasarkan penerima saat ini.

Recap

  • React keeps state for as long as the same component is rendered at the same position.
  • State is not kept in JSX tags. It’s associated with the tree position in which you put that JSX.
  • You can force a subtree to reset its state by giving it a different key.
  • Don’t nest component definitions, or you’ll reset state by accident.

Challenge 1 of 5:
Fix disappearing input text

This example shows a message when you press the button. However, pressing the button also accidentally resets the input. Why does this happen? Fix it so that pressing the button does not reset the input text.

import { useState } from 'react';

export default function App() {
  const [showHint, setShowHint] = useState(false);
  if (showHint) {
    return (
      <div>
        <p><i>Hint: Your favorite city?</i></p>
        <Form />
        <button onClick={() => {
          setShowHint(false);
        }}>Hide hint</button>
      </div>
    );
  }
  return (
    <div>
      <Form />
      <button onClick={() => {
        setShowHint(true);
      }}>Show hint</button>
    </div>
  );
}

function Form() {
  const [text, setText] = useState('');
  return (
    <textarea
      value={text}
      onChange={e => setText(e.target.value)}
    />
  );
}