将我们的 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 中处理事件,并开始添加一些交互性。