ReactJS - 鍵



列表和鍵

在之前的章節中,我們學習瞭如何使用 for 迴圈和 map 函式在 React 中使用集合。如果我們執行應用程式,它將按預期輸出。如果我們在瀏覽器中開啟開發者控制檯,則會顯示如下警告:

Warning: Each child in a list should have a unique "key" prop.
Check the render method of `ExpenseListUsingForLoop`. See https://reactjs.org.tw/link/warning-keys for more information.
tr
ExpenseListUsingForLoop@
div
App

那麼,這是什麼意思,它如何影響我們的 React 應用程式?眾所周知,React 嘗試透過各種機制僅渲染 DOM 中已更新的值。當 React 渲染集合時,它會嘗試透過僅更新列表中已更新的項來最佳化渲染。

但是,React 沒有提示來查詢哪些項是新的、已更新的或已刪除的。為了獲取資訊,React 允許為所有元件設定 key 屬性。唯一的要求是 key 的值在當前集合中必須唯一。

讓我們重新建立我們之前的應用程式之一併應用 key 屬性。

create-react-app myapp
cd myapp
npm start

接下來,在 components 資料夾下建立一個元件,**ExpenseListUsingForLoop** (src/components/ExpenseListUsingForLoop.js)。

import React from 'react'
class ExpenseListUsingForLoop extends React.Component {
   render() {
      return <table>
         <thead>
            <tr>
               <th>Item</th>
               <th>Amount</th>
            </tr>
         </thead>
         <tbody>
         </tbody>
         <tfoot>
            <tr>
               <th>Sum</th>
               <th></th>
            </tr>
         </tfoot>
      </table>
   }
}
export default ExpenseListUsingForLoop

在這裡,我們建立了一個帶有表頭和表尾的基本表格結構。然後,建立一個函式來查詢總支出金額。我們稍後會在 render 方法中使用它。

getTotalExpenses() {
   var items = this.props['expenses'];
   var total = 0;
   for(let i = 0; i < items.length; i++) {
      total += parseInt(items[i]);
   }
   return total;
}

在這裡,getTotalExpenses 遍歷 expense props 並彙總總支出。然後,在 render 方法中新增支出項和總金額。

render() {
   var items = this.props['expenses'];
   var expenses = []
   expenses = items.map((item, idx) => <tr key={idx}><td>item {idx + 1}</td><td>{item}</td></tr>)
   var total = this.getTotalExpenses();
   return <table>
      <thead>
         <tr>
            <th>Item</th>
            <th>Amount</th>
         </tr>
      </thead>
      <tbody>
         {expenses}
      </tbody>
      <tfoot>
         <tr>
            <th>Sum</th>
            <th>{total}</th>
         </tr>
      </tfoot>
   </table>
}

這裡:

  • 使用 map 函式遍歷 expense 陣列中的每個專案,使用轉換函式為每個條目建立表格行 (tr),最後將返回的陣列設定到 expenses 變數中。

  • 為每一行設定 key 屬性,其值為專案的索引值。

  • 在 JSX 表示式中使用 expenses 陣列來包含生成的 rows。

  • 使用 getTotalExpenses 方法查詢總支出金額並將其新增到 render 方法中。

ExpenseListUsingForLoop 元件的完整原始碼如下:

import React from 'react'
class ExpenseListUsingForLoop extends React.Component {
   getTotalExpenses() {
      var items = this.props['expenses'];
      var total = 0;
      for(let i = 0; i < items.length; i++) {
         total += parseInt(items[i]);
      }
      return total;
   }
   render() {
      var items = this.props['expenses'];
      var expenses = []
      expenses = items.map(
         (item, idx) => <tr key={idx}><td>item {idx + 1}</td><td>{item}</td></tr>)
      var total = this.getTotalExpenses();
      return <table>
         <thead>
            <tr>
               <th>Item</th>
               <th>Amount</th>
            </tr>
         </thead>
         <tbody>
            {expenses}
         </tbody>
         <tfoot>
            <tr>
               <th>Sum</th>
               <th>{total}</th>
            </tr>
         </tfoot>
      </table>
   }
}
export default ExpenseListUsingForLoop

接下來,使用 ExpenseListUsingForLoop 元件更新 App 元件 (App.js)。

import ExpenseListUsingForLoop from './components/ExpenseListUsingForLoop';
import './App.css';
function App() {
   var expenses = [100, 200, 300]
   return (
      <div>
         <ExpenseListUsingForLoop expenses={expenses} />
      </div>
   );
}
export default App;

接下來,在App.css中新增基本的樣式。

/* Center tables for demo */
table {
   margin: 0 auto;
}
div {
   padding: 5px;
}
/* Default Table Style */
table {
   color: #333;
   background: white;
   border: 1px solid grey;
   font-size: 12pt;
   border-collapse: collapse;
}
table thead th,
table tfoot th {
   color: #777;
   background: rgba(0,0,0,.1);
   text-align: left;
}
table caption {
   padding:.5em;
}
table th,
table td {
   padding: .5em;
   border: 1px solid lightgrey;
}

接下來,在瀏覽器中檢查應用程式。它將顯示如下支出:

List and Keys

最後,開啟開發者控制檯,發現沒有顯示關於 key 的警告。

鍵和索引

我們瞭解到 key 應該唯一以最佳化元件的渲染。我們使用了索引值,錯誤消失了。這仍然是為列表提供值的正確方法嗎?答案是肯定的和否定的。設定索引 key 在大多數情況下都能工作,但如果我們在應用程式中使用非受控元件,它會以意想不到的方式執行。

讓我們更新我們的應用程式並新增兩個如下所述的新功能:

  • 在支出金額旁邊新增一個輸入元素到每一行。

  • 新增一個按鈕以刪除列表中的第一個元素。

首先,新增一個建構函式並設定應用程式的初始狀態。因為我們將在應用程式的執行時刪除某些專案,所以我們應該使用狀態而不是 props。

constructor(props) {
   super(props)
   this.state = {
      expenses: this.props['expenses']
   }
}

接下來,新增一個函式來刪除列表的第一個元素。

remove() {
   var itemToRemove = this.state['expenses'][0]
   this.setState((previousState) => ({
      expenses: previousState['expenses'].filter((item) => item != itemToRemove)
   }))
}

接下來,在建構函式中繫結 remove 函式,如下所示:

constructor(props) {
   super(props)
   this.state = {
      expenses: this.props['expenses']
   }
   this.remove = this.remove.bind(this)
}

接下來,在表格下方新增一個按鈕,並在其 onClick 操作中設定 remove 函式。

render() {
   var items = this.state['expenses'];
   var expenses = []
   expenses = items.map(
      (item, idx) => <tr key={idx}><td>item {idx + 1}</td><td>{item}</td></tr>)
   var total = this.getTotalExpenses();
   return (
      <div>
         <table>
            <thead>
               <tr>
                  <th>Item</th>
                  <th>Amount</th>
               </tr>
            </thead>
            <tbody>
               {expenses}
            </tbody>
            <tfoot>
               <tr>
                  <th>Sum</th>
                  <th>{total}</th>
               </tr>
            </tfoot>
         </table>
         <div>
            <button onClick={this.remove}>Remove first item</button>
         </div>
      </div>
   )
}

接下來,在所有行中支出金額旁邊新增一個輸入元素,如下所示:

expenses = items.map((item, idx) => <tr key={idx}><td>item {idx + 1}</td><td>{item} <input /></td></tr>)

接下來,在瀏覽器中開啟應用程式。應用程式將如下渲染:鍵和索引

接下來,在第一個輸入框中輸入一個金額(例如,100),然後單擊“刪除第一個專案”按鈕。這將刪除第一個元素,但輸入的金額將填充到第二個元素(金額:200)旁邊的輸入框中,如下所示:

Key and Index

為了解決這個問題,我們應該刪除索引作為 key 的用法。相反,我們可以使用表示專案的唯一 ID。讓我們將專案從數字陣列更改為物件陣列,如下所示 (App.js):

import ExpenseListUsingForLoop from './components/ExpenseListUsingForLoop';
import './App.css';
function App() {
   var expenses = [
      {
         id: 1,
         amount: 100
      },
      {
         id: 2,
         amount: 200
      },
      {
         id: 3,
         amount: 300
      }
   ]
   return (
      <div>
      <ExpenseListUsingForLoop expenses={expenses} />
      </div>
   );
}
export default App;

接下來,透過將 item.id 設定為 key 來更新渲染邏輯,如下所示:

expenses = items.map((item, idx) => <tr key={item.id}><td>{item.id}</td><td>{item.amount} <input /></td></tr>)

接下來,更新 getTotalExpenses 邏輯,如下所示:

getTotalExpenses() {
   var items = this.props['expenses'];
   var total = 0;
   for(let i = 0; i < items.length; i++) {
      total += parseInt(items[i].amount);
   }
   return total;
}

在這裡,我們更改了從物件獲取金額的邏輯 (item[i].amount)。最後,在瀏覽器中開啟應用程式並嘗試重現之前的錯誤。在第一個輸入框中新增一個數字(例如 100),然後單擊“刪除第一個專案”。現在,第一個元素被刪除,並且輸入的值不會保留在下一個輸入框中,如下所示:

Key and Index
廣告