将我们的 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 应用程序应该基本上与之前一样渲染,但使用您闪亮的新组件。