將我們的 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
<Todo name="Eat" />
<Todo name="Sleep" />
<Todo name="Repeat" />

當你的瀏覽器重新整理時,你會看到……與之前完全相同的內容。我們給我們的 <Todo /> 提供了一些 props,但我們還沒有使用它們。讓我們回到 Todo.jsx 並解決這個問題。

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

一旦你確信你的元件正在獲取其 props,你可以透過讀取 props.nameEat 的每次出現替換為你的 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 /> 呼叫提供一個布林型別的 completed prop,並將其他兩個保留原樣。

jsx
<Todo name="Eat" completed />
<Todo name="Sleep" />
<Todo name="Repeat" />

與之前一樣,我們必須返回 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 /> 元件為每個任務提供了一個 id 屬性 todo-0。這由於幾個原因是不好的

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

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

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

jsx
<Todo name="Eat" id="todo-0" completed />
<Todo name="Sleep" id="todo-1" />
<Todo name="Repeat" id="todo-2" />

注意: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 const 上看到警告。此警告來自我們使用的 Vite 模板提供的 ESLint 配置,它不適用於此程式碼。你可以透過在 DATA const 上方新增一行 // 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 語句上方建立一個新的 const,名為 taskList。讓我們首先將 props.tasks 陣列中的每個任務轉換為其 name?. 運算子允許我們執行 可選鏈 以檢查 props.tasks 是否為 undefinednull,然後再嘗試建立一個新的任務名稱陣列

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

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

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 中處理事件,並開始新增一些互動性。