狀態:組件的記憶體

組件通常需要根據互動來改變螢幕上的內容。在表單中輸入文字應該更新輸入欄位,點擊圖片輪播上的「下一張」應該更改顯示的圖片,點擊「購買」應該將產品放入購物車。組件需要「記住」事情:目前的輸入值、目前的圖片、購物車。在 React 中,這種組件特定的記憶體稱為狀態

你將學到

  • 如何使用 useState Hook 新增狀態變數
  • useState Hook 傳回哪一對值
  • 如何新增多個狀態變數
  • 為什麼狀態稱為局部狀態

何時一般變數不夠用

以下是一個渲染雕塑圖片的組件。點擊「下一張」按鈕應該將 index 更改為 1,然後是 2,依此類推,來顯示下一張雕塑。但是,這段程式碼**無法正常運作**(你可以試試看!)

import { sculptureList } from './data.js';

export default function Gallery() {
  let index = 0;

  function handleClick() {
    index = index + 1;
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
      <p>
        {sculpture.description}
      </p>
    </>
  );
}

handleClick 事件處理函式正在更新局部變數 index。但有兩個因素導致該變更無法顯示

  1. **局部變數在渲染之間不會持續存在。** 當 React 第二次渲染這個組件時,它會從頭開始渲染 — 它不會考慮對局部變數的任何更改。
  2. **局部變數的更改不會觸發渲染。** React 無法意識到它需要使用新資料再次渲染組件。

要使用新資料更新組件,需要發生兩件事

  1. **保留**渲染之間的資料。
  2. **觸發** React 使用新資料渲染組件(重新渲染)。

useState Hook 提供了這兩件事

  1. 一個**狀態變數**來保留渲染之間的資料。
  2. 一個**狀態設定函式**來更新變數並觸發 React 再次渲染組件。

新增狀態變數

要新增狀態變數,請在檔案頂端從 React 匯入 useState

import { useState } from 'react';

然後,將這一行程式碼

let index = 0;

取代為

const [index, setIndex] = useState(0);

index 是一個狀態變數,而 setIndex 是設定函式。

這裡的 [] 語法稱為 陣列解構,它允許你從陣列中讀取值。useState 傳回的陣列始終只有兩個項目。

以下是它們在 handleClick 中的協作方式

function handleClick() {
setIndex(index + 1);
}

現在點擊「下一張」按鈕會切換目前的雕塑

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);

  function handleClick() {
    setIndex(index + 1);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
      <p>
        {sculpture.description}
      </p>
    </>
  );
}

認識你的第一個 Hook

在 React 中,useState,以及任何其他以「use」開頭的函式,都稱為 Hook。

_Hooks_ 是特殊的函式,僅在 React 進行 渲染(我們將在下一頁詳細介紹)時可用。它們讓您可以「hook into」不同的 React 功能。

狀態只是其中一項功能,但您稍後會遇到其他 Hooks。

陷阱

Hooks—以 use 開頭的函式—只能在組件的頂層或 您自己的 Hooks 中呼叫。 您不能在條件、迴圈或其他巢狀函式內呼叫 Hooks。Hooks 是函式,但將它們視為組件需求的無條件宣告會有所幫助。您在組件頂部「使用」React 功能,類似於您在檔案頂部「導入」模組的方式。

useState 的剖析

當您呼叫 useState 時,您是在告訴 React 您希望這個組件記住某些東西

const [index, setIndex] = useState(0);

在這種情況下,您希望 React 記住 index

注意事項

慣例是將此對命名為 const [something, setSomething]。您可以隨意命名,但慣例可以讓不同專案更容易理解。

useState 的唯一參數是狀態變數的 初始值。在此範例中,index 的初始值使用 useState(0) 設定為 0

每次組件渲染時,useState 都會提供一個包含兩個值的陣列

  1. 具有您儲存的值的 狀態變數 (index)。
  2. 可以更新狀態變數並觸發 React 再次渲染組件的 狀態設定器函式 (setIndex)。

以下是實際運作方式

const [index, setIndex] = useState(0);
  1. 您的組件第一次渲染。 因為您將 0 傳遞給 useState 作為 index 的初始值,它將返回 [0, setIndex]。React 記住 0 是最新的狀態值。
  2. 您更新狀態。 當使用者點擊按鈕時,它會呼叫 setIndex(index + 1)index0,所以它是 setIndex(1)。這告訴 React 現在記住 index1 並觸發另一個渲染。
  3. 您的組件的第二次渲染。 React 仍然看到 useState(0),但因為 React _記住_ 您將 index 設定為 1,它會返回 [1, setIndex]
  4. 等等!

賦予組件多個狀態變數

您可以在一個組件中擁有任意多種類型的狀態變數。這個組件有兩個狀態變數,一個數字 index 和一個布林值 showMore,當您點擊「顯示詳細資訊」時會切換

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
    </>
  );
}

如果狀態無關,例如本例中的 indexshowMore,則最好有多個狀態變數。但是,如果您發現經常一起更改兩個狀態變數,將它們組合成一個可能會更容易。例如,如果您有一個包含許多欄位的表單,則擁有一個保存物件的狀態變數比每個欄位都有一個狀態變數更方便。閱讀 選擇狀態結構 以獲得更多提示。

深入探討

React 如何知道要返回哪個狀態?

您可能已經注意到,useState 呼叫沒有收到任何關於它引用 _哪個_ 狀態變數的資訊。沒有傳遞給 useState 的「識別符號」,那麼它如何知道要返回哪個狀態變數?它是否依賴於某些魔術,例如解析您的函式?答案是不。

相反,為了啟用其簡潔的語法,Hooks 依賴於相同組件每次渲染時穩定的呼叫順序。 這在實務中效果很好,因為如果您遵循上述規則(「僅在頂層呼叫 Hooks」),Hooks 將始終以相同的順序呼叫。此外,linter 插件 可以捕獲大多數錯誤。

在內部,React 為每個組件保存一個狀態對陣列。它還維護目前的對索引,在渲染前設定為 0。每次您呼叫 useState 時,React 都會提供下一個狀態對並增加索引。您可以在 React Hooks:不是魔術,只是陣列 中閱讀有關此機制的更多資訊。

此範例 未使用 React,但它讓您了解 useState 在內部如何運作

let componentHooks = [];
let currentHookIndex = 0;

// How useState works inside React (simplified).
function useState(initialState) {
  let pair = componentHooks[currentHookIndex];
  if (pair) {
    // This is not the first render,
    // so the state pair already exists.
    // Return it and prepare for next Hook call.
    currentHookIndex++;
    return pair;
  }

  // This is the first time we're rendering,
  // so create a state pair and store it.
  pair = [initialState, setState];

  function setState(nextState) {
    // When the user requests a state change,
    // put the new value into the pair.
    pair[0] = nextState;
    updateDOM();
  }

  // Store the pair for future renders
  // and prepare for the next Hook call.
  componentHooks[currentHookIndex] = pair;
  currentHookIndex++;
  return pair;
}

function Gallery() {
  // Each useState() call will get the next pair.
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  // This example doesn't use React, so
  // return an output object instead of JSX.
  return {
    onNextClick: handleNextClick,
    onMoreClick: handleMoreClick,
    header: `${sculpture.name} by ${sculpture.artist}`,
    counter: `${index + 1} of ${sculptureList.length}`,
    more: `${showMore ? 'Hide' : 'Show'} details`,
    description: showMore ? sculpture.description : null,
    imageSrc: sculpture.url,
    imageAlt: sculpture.alt
  };
}

function updateDOM() {
  // Reset the current Hook index
  // before rendering the component.
  currentHookIndex = 0;
  let output = Gallery();

  // Update the DOM to match the output.
  // This is the part React does for you.
  nextButton.onclick = output.onNextClick;
  header.textContent = output.header;
  moreButton.onclick = output.onMoreClick;
  moreButton.textContent = output.more;
  image.src = output.imageSrc;
  image.alt = output.imageAlt;
  if (output.description !== null) {
    description.textContent = output.description;
    description.style.display = '';
  } else {
    description.style.display = 'none';
  }
}

let nextButton = document.getElementById('nextButton');
let header = document.getElementById('header');
let moreButton = document.getElementById('moreButton');
let description = document.getElementById('description');
let image = document.getElementById('image');
let sculptureList = [{
  name: 'Homenaje a la Neurocirugía',
  artist: 'Marta Colvin Andrade',
  description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',
  url: 'https://i.imgur.com/Mx7dA2Y.jpg',
  alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'  
}, {
  name: 'Floralis Genérica',
  artist: 'Eduardo Catalano',
  description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',
  url: 'https://i.imgur.com/ZF6s192m.jpg',
  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'
}, {
  name: 'Eternal Presence',
  artist: 'John Woodrow Wilson',
  description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as "a symbolic Black presence infused with a sense of universal humanity."',
  url: 'https://i.imgur.com/aTtVpES.jpg',
  alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'
}, {
  name: 'Moai',
  artist: 'Unknown Artist',
  description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',
  url: 'https://i.imgur.com/RCwLEoQm.jpg',
  alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'
}, {
  name: 'Blue Nana',
  artist: 'Niki de Saint Phalle',
  description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',
  url: 'https://i.imgur.com/Sd1AgUOm.jpg',
  alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'
}, {
  name: 'Ultimate Form',
  artist: 'Barbara Hepworth',
  description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',
  url: 'https://i.imgur.com/2heNQDcm.jpg',
  alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'
}, {
  name: 'Cavaliere',
  artist: 'Lamidi Olonade Fakeye',
  description: "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
  url: 'https://i.imgur.com/wIdGuZwm.png',
  alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'
}, {
  name: 'Big Bellies',
  artist: 'Alina Szapocznikow',
  description: "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
  url: 'https://i.imgur.com/AlHTAdDm.jpg',
  alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'
}, {
  name: 'Terracotta Army',
  artist: 'Unknown Artist',
  description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',
  url: 'https://i.imgur.com/HMFmH6m.jpg',
  alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'
}, {
  name: 'Lunar Landscape',
  artist: 'Louise Nevelson',
  description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',
  url: 'https://i.imgur.com/rN7hY6om.jpg',
  alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'
}, {
  name: 'Aureole',
  artist: 'Ranjani Shettar',
  description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a "fine synthesis of unlikely materials."',
  url: 'https://i.imgur.com/okTpbHhm.jpg',
  alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'
}, {
  name: 'Hippos',
  artist: 'Taipei Zoo',
  description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',
  url: 'https://i.imgur.com/6o5Vuyu.jpg',
  alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'
}];

// Make UI match the initial state.
updateDOM();

即使不理解它,您也可以使用 React,但您可能會發現這是一個有用的心智模型。

狀態是隔離且私有的

狀態是螢幕上元件實例的局部狀態。換句話說,**如果您將同一個元件渲染兩次,每個副本將具有完全隔離的狀態!**更改其中一個不會影響另一個。

在此範例中,先前使用的 `Gallery` 元件渲染了兩次,其邏輯沒有任何變化。嘗試點擊每個畫廊內的按鈕。請注意,它們的狀態是獨立的。

import Gallery from './Gallery.js';

export default function Page() {
  return (
    <div className="Page">
      <Gallery />
      <Gallery />
    </div>
  );
}

這就是狀態與您可能在模組頂部宣告的常規變數不同之處。狀態不與特定函數呼叫或程式碼中的位置綁定,而是與螢幕上的特定位置「局部」相關。您渲染了兩個 `<Gallery />` 元件,因此它們的狀態是分開儲存的。

另請注意 `Page` 元件如何不「知道」任何關於 `Gallery` 狀態的事情,甚至不知道它是否有任何狀態。與 props 不同,**狀態對宣告它的元件完全私有。**父元件無法更改它。這讓您可以將狀態新增到任何元件或移除它,而不會影響其他元件。

如果您希望兩個畫廊的狀態保持同步怎麼辦?在 React 中正確的做法是從子元件中*移除*狀態,並將其新增到它們最近的共享父元件中。接下來的幾頁將重點介紹如何組織單個元件的狀態,但我們將在在元件之間共享狀態中回到這個主題。

重點回顧

  • 當元件需要在渲染之間「記住」某些資訊時,請使用狀態變數。
  • 狀態變數是透過呼叫 `useState` Hook 宣告的。
  • Hooks 是以 `use` 開頭的特殊函數。它們讓您可以「鉤入」React 功能,例如狀態。
  • Hooks 可能會讓您想起導入:它們需要無條件地被呼叫。呼叫 Hooks,包括 `useState`,只在元件的頂層或另一個 Hook 中有效。
  • `useState` Hook 會傳回一對值:目前的狀態和更新它的函數。
  • 您可以擁有多個狀態變數。在內部,React 會根據它們的順序將它們配對。
  • 狀態對元件是私有的。如果您在兩個地方渲染它,每個副本都會獲得自己的狀態。

當您按下最後一個雕塑上的「下一個」時,程式碼會崩潰。修復邏輯以防止崩潰。您可以透過向事件處理常式新增額外邏輯或在動作不可能時停用按鈕來執行此操作。

修復崩潰後,新增一個「上一個」按鈕來顯示上一個雕塑。它不應該在第一個雕塑上崩潰。

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
    </>
  );
}