State:Component 的記憶

作為互動的一環, component 需要經常轉換屏幕上的內容。比如在表格上輸入內容時應該要更新輸入欄位、在圖片輪播(image carousel)上按下「下一張」時應該要轉換顯示的圖片、而按下「購買」時則應該要把商品加到購物車。 Components 需要「記住」這些東西:當前的輸入欄位值、當前的圖片和購物車。在 React 裏,我們會稱呼這種 component 專用的記憶為 state

You will learn

  • 如何利用 useState Hook 將 state 變量加入至 component 中
  • 那些值會被 useState Hook 返回
  • 如何新增多種 state 變量
  • 為什麼 state 會被稱為局部

當一個常規變量並不足夠

下面展示了一個負責渲染雕塑的 component。當你按下 “Next” 時,會透過把 index 轉換成 1 來展示下一個雕塑,然後到 2,反之亦然。但實際上,這個 component 並不能運作(你可以試一試!):

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 第二次渲染這個 component 時,它是重頭開始渲染(renders it from scratch)的,並不會理會渲染之前所進行的任何改變。
  2. 當一個局部變量被改變時,並不會觸發渲染機制。 這是由於 React 並不會意識到它需要利用那些新變量來去渲染一個新的 component 。

要利用新的數據更新 component ,有兩件事是你可以做的:

  1. 確保 數據在渲染之間「存活」下來。
  2. 觸發 React 的渲染機制,讓其利用新數據進行渲染(re-rendering)。

useState Hook 提供了這兩樣東西:

  1. 用於將數據從渲染之間保存的 state 變量
  2. 用於更新變量和觸發 Component 渲染的 state setter 函数

新增 state 變量

要新增 state 變量,請將 useState 匯入至文件頂端:

import { useState } from 'react';

然後將這一行:

let index = 0;

寫成

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

index 是 state 變量而 setIndex 則是 setter 函数。

[] 的語法在這裏被稱為數組解構,它能讓你從數組之中讀值。而在被 useState 所返回的數組中總會有2樣東西。

這是他們在 handleClick 之中協同的例子:

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

現在請透過點擊 “Next” 來轉換正在展示的雕塑:

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 處於渲染時可用的特殊函數(我們將在下一頁詳細介紹)。它們可以讓你“掛上”不同的React功能。

而 State 只是其中之一個功能,您稍後將會看到更多其他的 Hook。

Pitfall

Hooks—以use開頭的函數—只能在Components的頂層或您自己的 Hooks 。 您不能調用條件(conditions)、循環(loops)或其他嵌套函數內的 Hooks。 Hooks 是函數,但將它們視為有關 component 需求的無條件聲明會很有幫助。 您可以在 component 頂部”使用” React 功能,類似於在文件頂部 “import” 模塊的方式。

useState 的剖析

當你調用 useState 時,你是在告訴 React 你希望這個 component 去記住一些東西:

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

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

Note

我們通常會將這對命名為 const [something, setSomething] 。 您也可以將其命名為任何您喜歡的名稱,但是按照常規可以使事情在跨項目時更容易被理解。

useState 的唯一參數是 state 變量的初始值。 在此個示例中,index 的初始值通過 useState(0) 被設置為0

在每次渲染 component 時, useState 都會為您提供一個包含兩個值的數組:

  1. State變量 (index) 以及您存儲的值。
  2. State設置函數 (setIndex) 可以更新 State 變量並觸發 React 去再次渲染 component 。

下面是處理時所發生的事:

const [index, setIndex] = useState(0);
  1. 你的 component 首次被渲染。 因為你將0作為 useState 的初始值傳遞進去,它會返回 [0, setIndex] 。 React 記住了0是最新的 state。
  2. 你更新了狀態。 當用戶點擊按鈕時,它調用了 setIndex(index + 1)index0,所以它是 setIndex(1) 。現在 React 記住了 index1,並觸發了另一次渲染。
  3. 你組件的第二次渲染。 React 仍然看到 useState(0) ,但是因為 React 記住了你將 index 設置為 1 ,它因此返回了 [1, setIndex]
  4. 以此類推!

讓 component 有多個 state 變量

你可以在一個 component 中擁有任意數量和類型的 state 變量。這個 component 有兩個 state 變量,一個是數字(number) index ,另一個是布爾值(boolean) showMore ,當你點擊 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}
      />
    </>
  );
}

如果 state 變量之間並不相關,那麼最好會有多個 state 變量,例如本例中的 indexshowMore 。但如果您發現你經常一起更改兩個 state 變量時,則將它們合併為一個可能會更好。例如您有一個包含許多字段的表單,那麼使用一個 state 變量來保存一個 object 會比每個字段都有一個 state 變量更方便。 你可以閱讀選擇 State 結構來了解更多。

Deep Dive

React 是如何知道要返回哪個 state ?

您可能已經注意到, useState 調用並不會收到有關它引用的哪個 state 變量的任何信息。沒有傳遞給 useState 的“標識符”,那麼它是如何知道要返回哪一個 state 變量呢?它是否依賴於解析函數之類的“魔法”?不,并沒有。

相反,為了實現簡潔的語法, Hooks 依賴於同一個 component 上每次渲染的穩定調用順序。 實際上這效果很好,因為如果您有遵循上述規則(“僅在頂層調用 Hooks ”), Hooks 總是以相同的順序被調用。此外, linter 插件可以獲取大部分的錯誤。

在 React 內部,它為每個 component 都保存了一個 state 的數組。而且它還維護了當前的每對 index ,該 index 在渲染之前設置為 0。每次調用 useState 時, React 都會為您提供下一個 state pair 並遞增 index 。您可以在 React Hooks: Not Magic, Just Arrays. 中閱讀更多有關這個機制的信息。

這個例子 并沒有使用 React ,但它讓你了解到 useState 內部是如何運作的:

let componentHooks = [];
let currentHookIndex = 0;

// useState 在 React 中是如何工作的(簡化版)。
function useState(initialState) {
  let pair = componentHooks[currentHookIndex];
  if (pair) {
    // 這並不是第一次渲染
    // 所以 state pair 已經存在。
    // 返回它並準備下一次的 Hook 調用。
    currentHookIndex++;
    return pair;
  }

  // 這是第一次渲染,
  // 因此創建了一個 state pair 並存儲它。
  pair = [initialState, setState];

  function setState(nextState) {
    // 當用戶請求 state 改變時,
    // 將新值放入該 pair 中。
    pair[0] = nextState;
    updateDOM();
  }

  // 存儲該 pair 以供將來渲染
  // 並為下一次 Hook 調用做好準備。
  componentHooks[currentHookIndex] = pair;
  currentHookIndex++;
  return pair;
}

function Gallery() {
  // 每次 useState() 調用都會獲取下一對。
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

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

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

  let sculpture = sculptureList[index];
  // 這個例子沒有使用 React,
  // 所以返回了一個輸出對象而不是 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() {
  // 在渲染組件之前
  // 重置了當前 Hook index 。
  currentHookIndex = 0;
  let output = Gallery();

  // 更新 DOM 以匹配輸出。
  // 這就是 React 為你做的部分。
  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 ,但您會發現這是一個很有用的思維模型。

State 是隔離並私人的

State 在屏幕上 component 實例中是局部的。換句話說,如果同一個 component 渲染兩次,每個副本都會有完全隔離的 state ! 更改其中一個不會影響另一個。

在這個示例中,之前的 Gallery component 被渲染了兩次,但其邏輯沒有變化。你可以嘗試單擊每個畫廊內的按鈕。並留意到它們的 state 是獨立的:

import Gallery from './Gallery.js';

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

這就是 state 與您可以在 module 頂部聲明的常規變量不同的原因。 State 不與特定的函數調用或代碼中的位置相關,而是與屏幕上的特定位置“局部”相關聯。您渲染了兩個 <Gallery /> components ,因此它們的 state 是單獨存儲的。

還要注意 Page component 並“不知道”有關 Gallery state 的任何信息,甚至不知道它是否有任何 state。與 props 不同, state 對於聲明它的component來說是完全私有的。 父組件無法更改它。這使您可以向任何 component 添加或刪除 state ,而不會影響其餘 components 。

如果您希望兩個畫廊保持 states 同步怎麼辦?在 React 中執行此操作的正確方法是從子組件中刪除 state 並將其添加到最接近的共享父組件中。 接下來的幾頁將重點討論單個 component 的組織 state,但我們將在 在 Component 之間共享 State 中回到這個主題。

Recap

  • 當 component 需要在渲染之間“記住”一些信息時,使用 state 變量。
  • State 變量是通過調用 useState Hook 來聲明的。
  • Hooks 是以 use 開頭的特殊函數。它們讓你”掛上” React 功能,例如 state 等等。
  • Hooks 可能會讓您想起 imports :它們無需條件調用。調用 Hooks ,包括 useState ,僅在 component 或 Hook 的頂層有效。
  • useState Hook 返回一對值:當前 state 和更新它的函數。
  • 你可以有多個 state 變量。 在內部, React 會按順序匹配它們。
  • State 是 component 私有的。 如果在兩個位置渲染它,每個副本都會獲得自己的 state 。

當您在最後一個雕塑上按 “Next” 時,代碼崩潰了。嘗試修復邏輯以防止崩潰。您可以通過向事件處理程序添加額外的邏輯或在無法執行操作時禁用按鈕。

修復崩潰後,添加一個 “Previous” 按鈕,顯示上一個雕塑。而它不應該在第一個雕塑時崩潰。

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}
      />
    </>
  );
}