將我們的 React 應用元件化

到目前為止,我們的應用還是一個整體。在讓它做任何事情之前,我們需要將其分解成可管理、描述性的元件。React 對於什麼是元件,什麼不是元件並沒有硬性規定——這取決於你!在本文中,我們將向你展示一種將應用分解成元件的合理方式。

預備知識 熟悉核心 HTMLCSSJavaScript 語言,以及 終端/命令列
學習成果 將我們的待辦事項列表應用分解成元件的合理方式。

定義我們的第一個元件

定義元件在實踐之前可能看起來很棘手,但要點是:

  • 如果它代表你應用中一個明顯的“塊”,那它可能是一個元件。
  • 如果它經常被重複使用,那它可能是一個元件。

第二個要點尤其有價值:將常見的 UI 元素做成元件,可以讓你在一個地方修改程式碼,並在所有使用該元件的地方看到這些變化。你也不必立即將所有東西都分解成元件。讓我們以第二個要點為靈感,將 UI 中最常用、最重要的部分——待辦事項列表項——製作成一個元件。

建立一個 <Todo />

在建立元件之前,我們應該為它建立一個新檔案。事實上,我們應該為元件建立一個目錄。在執行這些命令之前,請確保你位於應用的根目錄!

bash
# create a `components` directory
mkdir src/components
# within `components`, create a file called `Todo.jsx`
touch src/components/Todo.jsx

如果你停止了開發伺服器來執行之前的命令,別忘了重新啟動它!

讓我們在 Todo.jsx 中新增一個 Todo() 函式。在這裡,我們定義了一個函式並將其匯出:

jsx
function Todo() {}

export default Todo;

到目前為止還可以,但我們的元件應該返回一些有用的東西!回到 src/App.jsx,複製無序列表中的第一個 <li>,並將其貼上到 Todo.jsx 中,使其變為:

jsx
function Todo() {
  return (
    <li className="todo stack-small">
      <div className="c-cb">
        <input id="todo-0" type="checkbox" defaultChecked />
        <label className="todo-label" htmlFor="todo-0">
          Eat
        </label>
      </div>
      <div className="btn-group">
        <button type="button" className="btn">
          Edit <span className="visually-hidden">Eat</span>
        </button>
        <button type="button" className="btn btn__danger">
          Delete <span className="visually-hidden">Eat</span>
        </button>
      </div>
    </li>
  );
}

export default Todo;

現在我們有了一些可以使用的東西。在 App.jsx 中,在檔案頂部新增以下行以匯入 Todo

jsx
import Todo from "./components/Todo";

匯入此元件後,你可以將 App.jsx 中所有的 <li> 元素替換為 <Todo /> 元件呼叫。你的 <ul> 應該像這樣:

jsx
<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading">
  <Todo />
  <Todo />
  <Todo />
</ul>

當你回到你的應用時,你會發現一些不幸的事情:你的列表現在重複了第一個任務三次!

Our todo list app, with todo components repeating because the label is hardcoded into the component

我們不只是想吃飯;我們還有其他事情要做。接下來我們將看看如何讓不同的元件呼叫渲染獨特的內容。

建立一個獨特的 <Todo />

元件之所以強大,是因為它們允許我們重複使用 UI 的片段,並引用一個地方作為該 UI 的來源。問題是,我們通常不想重複使用每個元件的所有部分;我們想重複使用大部分,並更改小部分。這就是 props 的作用。

name 中有什麼?

為了跟蹤我們想要完成的任務名稱,我們應該確保每個 <Todo /> 元件都渲染一個唯一的名稱。

App.jsx 中,給每個 <Todo /> 一個 name prop。讓我們使用我們之前的任務名稱:

jsx
<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading">
  <Todo name="Eat" />
  <Todo name="Sleep" />
  <Todo name="Repeat" />
</ul>

當你的瀏覽器重新整理時,你會看到……和以前一模一樣。我們給 <Todo /> 傳入了一些 props,但我們還沒有使用它們。讓我們回到 Todo.jsx 來解決這個問題。

首先修改你的 Todo() 函式定義,使其將 props 作為引數。如果你想檢查 props 是否被元件正確接收,可以 console.log() 你的 props。

一旦你確信你的元件正在獲取其 props,你就可以透過讀取 props.name 來替換所有出現的 Eat 為你的 name prop。記住:props.name 是一個 JSX 表示式,所以你需要將其用花括號包裹起來。

把所有這些放在一起,你的 Todo() 函式應該像這樣:

jsx
function Todo(props) {
  return (
    <li className="todo stack-small">
      <div className="c-cb">
        <input id="todo-0" type="checkbox" defaultChecked={true} />
        <label className="todo-label" htmlFor="todo-0">
          {props.name}
        </label>
      </div>
      <div className="btn-group">
        <button type="button" className="btn">
          Edit <span className="visually-hidden">{props.name}</span>
        </button>
        <button type="button" className="btn btn__danger">
          Delete <span className="visually-hidden">{props.name}</span>
        </button>
      </div>
    </li>
  );
}

export default Todo;

現在你的瀏覽器應該顯示三個獨特的任務。然而,還有一個問題:它們預設仍然都是勾選狀態。

Our todo list, with different todo labels now they are passed into the components as props

completed 了嗎?

在我們最初的靜態列表中,只有“Eat”被選中。同樣,我們希望重複使用構成 <Todo /> 元件的大部分 UI,但只改變一件事。這正是另一個 prop 的好用之處!給你的第一個 <Todo /> 呼叫一個布林 prop completed,並將其餘兩個保持原樣。

jsx
<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading">
  <Todo name="Eat" completed />
  <Todo name="Sleep" />
  <Todo name="Repeat" />
</ul>

和以前一樣,我們必須回到 Todo.jsx 來實際使用這些 props。更改 <input /> 上的 defaultChecked 屬性,使其值等於 completed prop。完成後,Todo 元件的 <input /> 元素將變為:

jsx
<input id="todo-0" type="checkbox" defaultChecked={props.completed} />

你的瀏覽器應該會更新,只顯示“Eat”被選中。

Our todo list app, now with differing checked states - some checkboxes are checked, others not

如果你改變每個 <Todo /> 元件的 completed prop,你的瀏覽器將相應地勾選或取消勾選等效渲染的複選框。

請給我一些 id

我們還有另一個問題:我們的 <Todo /> 元件給每個任務都賦予了 todo-0id 屬性。這有幾個不好的原因:

  • id 屬性必須是唯一的(它們被 CSS、JavaScript 等用作文件片段的唯一識別符號)。
  • id 不唯一時,label 元素的功能可能會失效。

第二個問題正在影響我們的應用。如果你點選第二個複選框旁邊的“Sleep”字樣,你會注意到“Eat”複選框被切換了,而不是“Sleep”複選框。這是因為每個複選框的 <label> 元素都有一個 htmlFor 屬性,其值為 todo-0<label> 元素只識別具有給定 id 屬性的第一個元素,這導致你點選其他標籤時看到的問題。

在建立 <Todo /> 元件之前,我們有唯一的 id 屬性。讓我們按照 todo-i 的格式將它們帶回來,其中 i 每次增加一。更新 App.jsxTodo 元件例項以新增 id props,如下所示:

jsx
<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading">
  <Todo name="Eat" id="todo-0" completed />
  <Todo name="Sleep" id="todo-1" />
  <Todo name="Repeat" id="todo-2" />
</ul>

注意:這裡的 completed prop 排在最後,因為它是一個沒有賦值的布林值。這純粹是一種風格約定。props 的順序無關緊要,因為 props 是 JavaScript 物件,而 JavaScript 物件是無序的。

現在回到 Todo.jsx 並使用 id prop。它需要替換 <input /> 元素的 id 屬性值以及其 <label>htmlFor 屬性值:

jsx
<div className="c-cb">
  <input id={props.id} type="checkbox" defaultChecked={props.completed} />
  <label className="todo-label" htmlFor={props.id}>
    {props.name}
  </label>
</div>

有了這些修復,點選每個複選框旁邊的標籤將如我們預期般操作——勾選和取消勾選這些標籤旁邊的複選框。

到目前為止一切順利嗎?

到目前為止,我們已經很好地使用了 React,但我們可以做得更好!我們的程式碼重複性很高。渲染 <Todo /> 元件的三行程式碼幾乎完全相同,只有一個區別:每個 prop 的值。

我們可以利用 JavaScript 的核心能力之一:迭代,來清理我們的程式碼。要使用迭代,我們首先應該重新思考我們的任務。

任務作為資料

我們每個任務目前包含三條資訊:它的名稱、它是否已被選中以及它的唯一 ID。這些資料很好地轉換為一個物件。由於我們有多個任務,一個物件陣列將很好地表示這些資料。

src/main.jsx 中,在最終匯入下方但在 ReactDOM.createRoot() 上方宣告一個新的 const

jsx
const DATA = [
  { id: "todo-0", name: "Eat", completed: true },
  { id: "todo-1", name: "Sleep", completed: false },
  { id: "todo-2", name: "Repeat", completed: false },
];

注意:如果你的文字編輯器有 ESLint 外掛,你可能會在這個 DATA 常量上看到一個警告。這個警告來自我們使用的 Vite 模板提供的 ESLint 配置,它不適用於此程式碼。你可以透過在 DATA 常量上面新增 // eslint-disable-next-line 來安全地抑制警告。

接下來,我們將 DATA 作為名為 tasks 的 prop 傳遞給 <App />。更新 src/main.jsx 中你的 <App /> 元件呼叫,使其變為:

jsx
<App tasks={DATA} />

DATA 陣列現在可以在 App 元件中作為 props.tasks 使用。如果你想檢查,可以 console.log() 它。

注意: ALL_CAPS 常量名稱在 JavaScript 中沒有特殊含義;它們是一種約定,告訴其他開發者“這些資料在定義後永遠不會改變”。

使用迭代進行渲染

要渲染我們的物件陣列,我們必須將每個物件轉換為一個 <Todo /> 元件。JavaScript 為我們提供了一個將專案轉換為其他東西的陣列方法:Array.prototype.map()

App.jsx 中,在 App() 函式的 return 語句上方建立一個名為 taskList 的新 const。讓我們首先將 props.tasks 陣列中的每個任務轉換為其 name?. 運算子允許我們執行可選鏈式操作,以檢查 props.tasks 在嘗試建立任務名稱的新陣列之前是否為 undefinednull

jsx
const taskList = props.tasks?.map((task) => task.name);

讓我們嘗試將 <ul> 的所有子元素替換為 taskList

jsx
<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading">
  {taskList}
</ul>

這讓我們在重新顯示所有元件的路上走了一段,但我們還有更多工作要做:瀏覽器目前將每個任務的名稱渲染為純文字。我們缺少 HTML 結構——<li> 以及它的複選框和按鈕!

Our todo list app with the todo item labels just shown bunched up on one line

為了解決這個問題,我們需要從 map() 函式返回一個 <Todo /> 元件——記住 JSX 是 JavaScript,所以我們可以將其與任何其他更熟悉的 JavaScript 語法一起使用。讓我們嘗試以下程式碼,而不是我們已有的程式碼:

jsx
const taskList = props.tasks?.map((task) => <Todo />);

再看看你的應用程式;現在我們的任務看起來更像以前了,但它們缺少任務本身的名稱。請記住,我們對映的每個任務都包含我們想要傳遞給 <Todo /> 元件的 idnamecompleted 屬性。如果我們把這些知識結合起來,我們就會得到這樣的程式碼:

jsx
const taskList = props.tasks?.map((task) => (
  <Todo id={task.id} name={task.name} completed={task.completed} />
));

現在應用看起來和以前一樣,並且我們的程式碼重複性更低。

唯一的鍵

既然 React 正在從陣列中渲染我們的任務,它必須跟蹤哪個是哪個以便正確渲染它們。React 會嘗試自行猜測來跟蹤事物,但我們可以透過將 key prop 傳遞給我們的 <Todo /> 元件來幫助它。key 是一個由 React 管理的特殊 prop —— 你不能將 key 這個詞用於任何其他目的。

因為鍵必須是唯一的,我們將重用每個任務物件的 id 作為其鍵。像這樣更新你的 taskList 常量:

jsx
const taskList = props.tasks?.map((task) => (
  <Todo
    id={task.id}
    name={task.name}
    completed={task.completed}
    key={task.id}
  />
));

你總是應該為透過迭代渲染的任何內容傳遞一個唯一的鍵。你的瀏覽器中不會發生任何明顯的變化,但如果你不使用唯一的鍵,React 會在控制檯中記錄警告,並且你的應用可能會出現異常行為!

將應用的其餘部分元件化

現在我們已經解決了最重要的元件,我們可以將應用的其餘部分轉換為元件。記住元件要麼是明顯的 UI 片段,要麼是重用的 UI 片段,或者兩者兼有,我們可以再建立兩個元件:

  • <Form />
  • <FilterButton />

既然我們知道兩者都需要,我們可以在一個終端命令中批次完成一些檔案建立工作。在你的終端中執行此命令,注意你處於應用的根目錄中:

bash
touch src/components/{Form,FilterButton}.jsx

<Form />

開啟 components/Form.jsx 並執行以下操作:

  • 宣告一個 Form() 函式並在檔案末尾匯出它。
  • App.jsx 中複製 <form> 標籤及其之間的所有內容,並將其貼上到 Form()return 語句中。

你的 Form.jsx 檔案應該像這樣:

jsx
function Form() {
  return (
    <form>
      <h2 className="label-wrapper">
        <label htmlFor="new-todo-input" className="label__lg">
          What needs to be done?
        </label>
      </h2>
      <input
        type="text"
        id="new-todo-input"
        className="input input__lg"
        name="text"
        autoComplete="off"
      />
      <button type="submit" className="btn btn__primary btn__lg">
        Add
      </button>
    </form>
  );
}

export default Form;

<FilterButton />

FilterButton.jsx 中執行與建立 Form.jsx 相同的操作,但將元件命名為 FilterButton(),並將 App.jsx<div className="filters btn-group stack-exception"> 內部的第一個按鈕的 HTML 複製到 return 語句中。

檔案應像這樣:

jsx
function FilterButton() {
  return (
    <button type="button" className="btn toggle-btn" aria-pressed="true">
      <span className="visually-hidden">Show </span>
      <span>all </span>
      <span className="visually-hidden"> tasks</span>
    </button>
  );
}

export default FilterButton;

注意:你可能會注意到我們在這裡犯了與我們最初為 <Todo /> 元件犯的相同錯誤,即每個按鈕都將是相同的。沒關係!我們將在稍後,在回到過濾按鈕中修復此元件。

匯入所有元件

讓我們利用我們的新元件。在 App.jsx 的頂部新增更多的 import 語句,並引用我們剛剛建立的元件。然後,更新 App()return 語句,使其渲染我們的元件。

完成後,App.jsx 將如下所示:

jsx
import Form from "./components/Form";
import FilterButton from "./components/FilterButton";
import Todo from "./components/Todo";

function App(props) {
  const taskList = props.tasks?.map((task) => (
    <Todo
      id={task.id}
      name={task.name}
      completed={task.completed}
      key={task.id}
    />
  ));
  return (
    <div className="todoapp stack-large">
      <h1>TodoMatic</h1>
      <Form />
      <div className="filters btn-group stack-exception">
        <FilterButton />
        <FilterButton />
        <FilterButton />
      </div>
      <h2 id="list-heading">3 tasks remaining</h2>
      <ul
        role="list"
        className="todo-list stack-large stack-exception"
        aria-labelledby="list-heading">
        {taskList}
      </ul>
    </div>
  );
}

export default App;

有了這些,你的 React 應用應該會和以前基本相同地渲染,但使用了你閃亮的新元件。

總結

本文到此為止——我們深入探討了如何將你的應用很好地分解成元件並有效地渲染它們。接下來我們將研究如何在 React 中處理事件,並開始新增一些互動性。