將我們的 React 應用元件化
目前,我們的應用程式是一個整體。在讓它執行操作之前,我們需要將其分解成可管理的、描述性的元件。React 對於什麼是元件、什麼不是元件沒有任何硬性規定——這取決於你!在本文中,我們將向你展示一種合理的方法來將我們的應用程式分解成元件。
| 先決條件 |
熟悉核心 HTML、CSS 和 JavaScript 語言,瞭解 終端/命令列。 |
|---|---|
| 目標 | 展示一種將我們的待辦事項列表應用程式分解成元件的合理方法。 |
定義我們的第一個元件
定義元件在沒有練習之前可能看起來很棘手,但要點是
- 如果它代表了應用程式的一個明顯的“塊”,它可能是一個元件
- 如果它經常被重用,它可能是一個元件。
第二個要點特別有價值:將常用 UI 元素製作成元件,允許你在一個地方更改程式碼,並在使用該元件的所有地方看到這些更改。你也不必立即將所有內容都分解成元件。讓我們以第二個要點為靈感,並從 UI 中最常使用、最重要的部分建立一個元件:待辦事項列表項。
建立一個 <Todo />
在建立元件之前,我們應該為它建立一個新檔案。事實上,我們應該為我們的元件建立一個目錄。確保在執行這些命令之前,你位於應用程式的根目錄!
# create a `components` directory
mkdir src/components
# within `components`, create a file called `Todo.jsx`
touch src/components/Todo.jsx
如果你停止了伺服器以執行之前的命令,請不要忘記重新啟動你的開發伺服器!
讓我們在 Todo.jsx 中新增一個 Todo() 函式。在這裡,我們定義一個函式並將其匯出
function Todo() {}
export default Todo;
到目前為止,這還可以,但我們的元件應該返回一些有用的東西!返回 src/App.jsx,複製無序列表內部的第一個 <li>,並將其貼上到 Todo.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
import Todo from "./components/Todo";
匯入此元件後,你可以將 App.jsx 中的所有 <li> 元素替換為 <Todo /> 元件呼叫。你的 <ul> 應該如下所示
<ul
role="list"
className="todo-list stack-large stack-exception"
aria-labelledby="list-heading">
<Todo />
<Todo />
<Todo />
</ul>
當你返回應用程式時,你會注意到一些不幸的事情:你的列表現在重複了第一個任務三次!
我們不僅想吃飯;我們還有其他事情要做——嗯——要做。接下來,我們將瞭解如何使不同的元件呼叫呈現唯一的內容。
建立一個唯一的 <Todo />
元件功能強大,因為它們允許我們重用 UI 的部分,並參考 UI 源的一個位置。問題是,我們通常不希望重用每個元件的所有部分;我們希望重用大部分部分,並更改少量部分。這就是 props 發揮作用的地方。
name 中有什麼?
為了跟蹤我們想要完成的任務的名稱,我們應該確保每個 <Todo /> 元件都呈現一個唯一的名稱。
在 App.jsx 中,為每個 <Todo /> 提供一個 name prop。讓我們使用我們之前擁有的任務名稱
<Todo name="Eat" />
<Todo name="Sleep" />
<Todo name="Repeat" />
當你的瀏覽器重新整理時,你會看到……與之前完全相同的內容。我們給我們的 <Todo /> 提供了一些 props,但我們還沒有使用它們。讓我們回到 Todo.jsx 並解決這個問題。
首先修改你的 Todo() 函式定義,使其將 props 作為引數。如果你想檢查 props 是否被元件正確接收,可以 console.log() 你的 props。
一旦你確信你的元件正在獲取其 props,你可以透過讀取 props.name 將 Eat 的每次出現替換為你的 name prop。請記住:props.name 是一個 JSX 表示式,因此你需要將其括在花括號中。
將所有這些放在一起,你的 Todo() 函式應該如下所示
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;
現在你的瀏覽器應該顯示三個唯一任務。但是,另一個問題仍然存在:它們預設都處於選中狀態。
是 completed 嗎?
在我們的原始靜態列表中,只有 Eat 被選中。再次,我們希望重用構成 <Todo /> 元件的大部分 UI,但要更改一項內容。這是另一個 prop 的好工作!為你的第一個 <Todo /> 呼叫提供一個布林型別的 completed prop,並將其他兩個保留原樣。
<Todo name="Eat" completed />
<Todo name="Sleep" />
<Todo name="Repeat" />
與之前一樣,我們必須返回 Todo.jsx 以實際使用這些 props。更改 <input /> 上的 defaultChecked 屬性,使其值等於 completed prop。完成後,Todo 元件的 <input /> 元素將如下所示
<input id="todo-0" type="checkbox" defaultChecked={props.completed} />
並且你的瀏覽器應該更新以僅顯示 Eat 被選中
如果你更改每個 <Todo /> 元件的 completed prop,你的瀏覽器將相應地選中或取消選中等效的渲染複選框。
請給我一些 id
我們仍然有另一個問題:我們的 <Todo /> 元件為每個任務提供了一個 id 屬性 todo-0。這由於幾個原因是不好的
第二個問題正在影響我們的應用程式。如果你點選第二個複選框旁邊的“Sleep”一詞,你會注意到“Eat”複選框切換而不是“Sleep”複選框。這是因為每個複選框的 <label> 元素都有一個 htmlFor 屬性 todo-0。<label> 僅識別具有給定 id 屬性的第一個元素,這會導致你在點選其他標籤時遇到的問題。
在我們建立 <Todo /> 元件之前,我們有唯一的 id 屬性。讓我們將它們帶回來,遵循 todo-i 的格式,其中 i 每次增加 1。更新 App.jsx 內的 Todo 元件例項以新增 id props,如下所示
<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 屬性值
<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
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 /> 元件呼叫,使其如下所示
<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 是否為 undefined 或 null,然後再嘗試建立一個新的任務名稱陣列
const taskList = props.tasks?.map((task) => task.name);
讓我們嘗試用 taskList 替換 <ul> 的所有子元素
<ul
role="list"
className="todo-list stack-large stack-exception"
aria-labelledby="list-heading">
{taskList}
</ul>
這讓我們在某種程度上重新顯示了所有元件,但我們還有更多工作要做:瀏覽器當前將每個任務的名稱呈現為純文字。我們缺少 HTML 結構——<li> 及其複選框和按鈕!
要解決此問題,我們需要從 map() 函式返回一個 <Todo /> 元件——請記住,JSX 是 JavaScript,因此我們可以將其與任何其他更熟悉的 JavaScript 語法一起使用。讓我們嘗試以下內容,而不是我們已經擁有的內容
const taskList = props.tasks?.map((task) => <Todo />);
再次檢視你的應用程式;現在我們的任務看起來更像以前一樣,但它們缺少任務本身的名稱。請記住,我們對映的每個任務都包含我們想要傳遞到 <Todo /> 元件的 id、name 和 completed 屬性。如果我們將這些知識放在一起,我們將得到如下程式碼
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 常量
const taskList = props.tasks?.map((task) => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
/>
));
你應該始終將唯一的鍵傳遞到你使用迭代渲染的任何內容。你的瀏覽器中不會發生明顯的更改,但如果你不使用唯一的鍵,React 會將警告記錄到你的控制檯,並且你的應用程式可能會出現奇怪的行為!
將應用程式的其餘部分元件化
現在我們已經解決了最重要的元件,我們可以將應用程式的其餘部分轉換為元件。記住,元件要麼是 UI 的明顯部分,要麼是 UI 的重用部分,或者兩者兼而有之,我們可以建立另外兩個元件
<Form /><FilterButton />
既然我們知道需要兩者,我們可以在一個終端命令中將一些檔案建立工作批次處理。在您的終端中執行此命令,注意您位於應用程式的根目錄中。
touch src/components/{Form,FilterButton}.jsx
<Form />
開啟 components/Form.jsx 並執行以下操作
- 宣告一個
Form()函式並在檔案末尾匯出它。 - 複製
App.jsx內部的<form>標籤及其之間的所有內容,並將它們貼上到Form()的return語句中。
您的 Form.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 語句中。
該檔案應該如下所示
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 將如下所示
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 應用程式應該基本上與之前一樣渲染,但使用您閃亮的新元件。