paint-brush
最大限度地发挥你的 React 技能:从头到尾构建一个待办事项列表应用程序(使用 TypeScript + Vite)经过@emotta
10,371 讀數
10,371 讀數

最大限度地发挥你的 React 技能:从头到尾构建一个待办事项列表应用程序(使用 TypeScript + Vite)

经过 Eduardo Motta de Moraes26m2023/02/02
Read on Terminal Reader

太長; 讀書

在这篇博文中,我们将介绍您理解和构建基本 React 应用程序所需的一切。无论您是刚开始使用 React 的初学者,还是希望提高技能的经验丰富的开发人员,本指南都适合您。
featured image - 最大限度地发挥你的 React 技能:从头到尾构建一个待办事项列表应用程序(使用 TypeScript + Vite)
Eduardo Motta de Moraes HackerNoon profile picture
0-item

在这篇博文中,我们将介绍您理解构建基本 React 应用程序所需的一切。无论您是刚开始使用 React 的初学者,还是希望提高技能的经验丰富的开发人员,本指南都适合您。


本指南将带您完成构建功能齐全的待办事项列表应用程序的整个过程,包括设计、布局、状态管理等。我们将使用功能组件和钩子。我们将学习如何使用 state 和 props 在组件之间传递数据,以及如何处理用户输入和更新应用程序的状态。


到本指南结束时,我们将对如何从头构建 React 应用程序有一个扎实的了解,您将能够利用新学到的知识来构建自己的 React 项目。

那么,让我们开始吧!


*您可以在此处找到我们将要构建的应用程序的代码,并在此处找到实时版本。

简介

我们将使用 TypeScript 编写代码,使用 Vite 开发和构建应用程序。

打字稿

TypeScript是一种建立在 JavaScript 之上的强类型编程语言。实际上,如果您已经了解 JavaScript,那么使用 TypeScript 所需要做的就是学习如何使用类型和接口。


类型和接口允许我们定义我们在代码中使用的数据类型。有了这个,我们可以及早发现错误并避免问题的发生。


例如,如果一个函数接受一个number ,但我们向它传递一个string ,TypeScript 会立即报错:


 const someFunc = (parameter: number) => {...}; someFunc('1') // Argument of type 'string' is not assignable to parameter of type 'number'.


如果我们使用 JavaScript,我们可能只会在稍后发现错误。


我们并不总是需要指定类型,因为 TypeScript 通常可以自动推断它们。


您可以在此处学习 TypeScript 的基础知识。 (或者只是忽略类型。)

维特

启动 React 应用程序的最常见方法可能是使用create-react-app 。我们将改用Vite (发音类似“veet”)。但不要担心,它同样简单,但效率更高。


使用像webpack这样的工具(由 create-react-app 在后台使用),您的整个应用程序需要捆绑在一个文件中,然后才能提供给浏览器。另一方面,Vite 利用浏览器中的原生 ES 模块,通过Rollup更高效地进行捆绑,根据需要提供部分源代码。


Vite 还可以通过热模块替换大大加快开发时间——这意味着每当对源代码进行更改时,只会更新更改,而不是整个应用程序。


除此之外,Vite 还提供对 Typescript、JSX 和 TSX、CSS 等的原生支持。


与 create-react-app 类似,Vite 提供了一个名为 create-vite 的工具,它允许我们使用基本模板(包括 Vanilla JS 的选项)或使用像 React 这样的库来快速启动一个新项目。


需要明确的是,我们不需要像 Vite 或 create-react-app 这样的工具来构建 React 应用程序,但它们通过负责设置项目、打包代码、使用转译器等让我们的生活更轻松。

深入 React

JSX / 多伦多证券交易所

React 允许我们直接在代码中添加标记,这些代码稍后将被编译为纯 JavaScript。这称为JSX 。当我们使用 JSX 时,我们可以将文件保存为 JavaScript 的 .jsx 或 TypeScript 的 .tsx。


它看起来像这样:


 const element = <h1>Hello, world!</h1>;


它类似于 HTML,但它嵌入在 JavaScript 文件中,它允许我们使用编程逻辑来操作标记。我们还可以在 JSX 中添加 JavaScript 代码,只要它在大括号内即可。


例如,如果我们有一个要呈现为不同段落元素的文本数组,我们可以这样做:


 const paragraphs = ["First", "Second", "Third"]; paragraphs.map((paragraph) => <p>{paragraph}</p>);


它会被编译成这样:


 <p>First</p> <p>Second</p> <p>Third</p>


但是,如果我们试图这样做,那是行不通的。那是因为 React 与组件一起工作,而 JSX 需要在这些组件内部呈现。

反应组件

React 组件可以使用 JavaScript或仅使用普通函数来编写。我们将专注于函数组件,因为它们是当今最新和推荐的编写 React 组件的方式。


一个组件由一个函数定义,该函数将返回将由浏览器编译和呈现的 JSX。所以扩展上面的例子,如果我们想渲染段落元素,它看起来像这样:


 // Define the component const Component = () => { const paragraphs = ["First", "Second", "Third"]; return ( <> {paragraphs.map((paragraph) => ( <p>{paragraph}</p> ))} </> ); }; // Use the component in the same way you use an HTML element in the JSX const OtherComponent = () => { return <Component />; };

道具

现在也许我们想用不同的信息重用这个组件。我们可以通过使用 props 来做到这一点——它只是一个保存一些数据的 JavaScript 对象。


在我们的示例中,我们可以将其传递给组件,而不是对数组进行硬编码。结果是一样的,但现在组件可以重用了。


如果我们使用 TypeScript,我们需要在 props 对象中指定数据的类型(它们是什么没有上下文,所以 TypeScript 无法推断它们),在这种情况下是一个字符串数组( string[] ).


 const Component = (props: { paragraphs: string[] }) => { <> {props.paragraphs.map((paragraph) => ( <p>{paragraph}</p> ))} </>; }; const OtherComponent = () => { const paragraphs = ["First", "Second", "Third"]; return <Component paragraphs={paragraphs} />; };

状态

如果我们想要制作一个交互式组件,我们将需要将信息存储在组件的状态中,以便它可以“记住”它。


例如,如果我们想要定义一个简单的计数器来显示按钮被点击的次数,我们需要一种存储和更新该值的方法。 React 允许我们使用useState挂钩(挂钩是一种让您“挂钩”到 React状态和生命周期特性的函数)。


我们用初始值调用useState钩子,它返回给我们一个包含值本身的数组和一个更新它的函数。


 import { useState } from "react"; const Counter = () => { const [count, setCount] = useState(0); return ( <> <span>{count}</span> <button onClick={() => setCount(count + 1)}>Increment count</button> </> ); };


有了这些知识,我们现在就可以开始构建我们的 React 应用程序了。

创建项目

依赖关系

要使用 Vite,我们需要 **node ** 和一个包管理器。


要安装节点,只需根据您的系统和配置选择此处的选项之一。如果您使用的是 Linux 或 Mac,您还可以使用Homebrew安装它。


包管理器可以是npmyarn 。在这篇文章中,我们将使用npm

创建项目

接下来是创建项目的时候了。在终端中,我们导航到将创建项目的目录,然后运行 create-vite 命令。


 $ npm create vite@latest


我们可能会被提示安装额外的包(如 create-vite)。输入y并按回车键继续。


 Need to install the following packages: [email protected] Ok to proceed? (y)


接下来我们将被提示输入项目信息。


输入项目的名称。我选择了my-react-project


 ? Project name: › my-react-project


选择React作为“框架”。


React 在技术上是一个库而不是一个框架,但不用担心。


 ? Select a framework: › - Use arrow-keys. Return to submit. Vanilla Vue ❯ React Preact Lit Svelte Others


选择TypeScript + SWC作为变体。


SWC (代表 Speedy Web Compiler )是一个用 Rust 编写的超快速 TypeScript / JavaScript 编译器。他们声称“在单线程上比 Babel 快 20 倍,在四核上快 70 倍”。


 ? Select a variant: › - Use arrow-keys. Return to submit. JavaScript TypeScript JavaScript + SWC ❯ TypeScript + SWC


大功告成,项目创建完毕。要以开发模式启动它,我们需要切换到项目目录,安装依赖项并运行 dev 脚本命令。


 cd my-react-project npm install npm run dev


几秒钟后,我们将看到类似这样的内容:


 VITE v4.0.4 ready in 486 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h to show help


如果我们打开浏览器并导航到http://localhost:5173 / 我们将看到默认的 Vite + React 页面:



这意味着一切都应该如此,我们可以开始开发我们的应用程序了。

构建应用程序

文件结构和初始设置

如果我们在我们选择的代码编辑器或 IDE 中打开项目,我们应该看到如下文件结构:



我们可以删除一些样板文件,因为我们不会使用它们(所有 .svg 和 .css 文件)。

可以删除 App 函数中的代码,这样我们就可以:


 function App() { return ( ) } export default App


我们稍后会回到这个文件。

造型

样式不是这里的重点,但我们将使用 Tailwind CSS,这是一个库,它允许我们通过向 HTML 元素添加类来设置样式。按照这些说明查看反映在您自己的项目中的样式。


否则你可以忽略代码中的类。

思考设计:组件布局

设计过程是应用程序开发的一个组成部分,不应被忽视。

要构建我们的待办事项列表应用程序,我们需要首先考虑组件布局。


我们首先模拟一个基本的 UI 并概述所涉及组件的层次结构。


如果您不是设计师,那么就颜色和精确位置而言,它不需要是完美的或最终的 UI — 更重要的是考虑组件结构。


理想情况下,我们的组件应该只负责一件事,遵循单一职责原则


在下图中,紫色的名称是我们将要构建的组件——其他所有内容都是本机 HTML 元素。如果他们在彼此的体内,这意味着可能会有亲子关系。

道具:构建静态版本

有了草图后,我们就可以开始构建应用程序的静态版本了。也就是说,只有 UI 元素,但还没有交互性。这部分非常简单,一旦掌握了它,就涉及大量的输入和很少的思考。


您可以在这个GitHub 存储库的分支“static-version”中找到静态版本的代码。完整运行的应用程序的代码是主分支。


容器


如上所述,我们将拥有一个可重复用于应用程序每个部分的容器。此容器展示了组合不同元素的一种方法:将它们作为子元素传递。


 // src/components/Container.tsx const Container = ({ children, title, }: { children: JSX.Element | JSX.Element[]; title?: string; }) => { return ( <div className="bg-green-600 p-4 border shadow rounded-md"> {title && <h2 className="text-xl pb-2 text-white">{title}</h2>} <div>{children}</div> </div> ); }; export default Container;


它需要一个带有JSX.Element | JSX.Element[]类型的children参数的 props 对象。 JSX.Element | JSX.Element[] 。这意味着我们可以将它与任何其他 HTML 元素或我们创建的任何其他组件组合在一起。它可以呈现在容器内我们想要的任何位置——在本例中是在第二个 div 内。


在我们的应用程序中,当我们在 App 组件中使用它们时,它将呈现每个部分(定义如下)。


Container 还带有一个名为title的可选string属性,只要它存在,它就会在 h2 中呈现。


 // src/App.tsx import Container from "./components/Container"; import Input from "./components/Input"; import Summary from "./components/Summary/Summary"; import Tasks from "./components/Tasks/Tasks"; function App() { return ( <div className="flex justify-center m-5"> <div className="flex flex-col items-center"> <div className="sm:w-[640px] border shadow p-10 flex flex-col gap-10"> <Container title={"Summary"}> <Summary /> </Container> <Container> <Input /> </Container> <Container title={"Tasks"}> <Tasks /> </Container> </div> </div> </div> ); } export default App;


概括


第一部分是摘要(Summary 组件),显示三个项目(SummaryItem):任务总数、待处理任务数和已完成任务数。这是另一种组合组件的方法:只需在另一个组件的返回语句中使用它们。


(不过,永远不要在另一个组件中定义一个组件很重要,因为这会导致不必要的重新渲染和错误。)


现在我们可以在两个组件中使用静态数据。


 // src/components/Summary/SummaryItem.tsx const SummaryItem = ({ itemName, itemValue, }: { itemName: string; itemValue: number; }) => { return ( <article className="bg-green-50 w-36 rounded-sm flex justify-between p-2"> <h3 className="font-bold">{itemName}</h3> <span className="bg-green-900 text-white px-2 rounded-sm"> {itemValue} </span> </article> ); }; export default SummaryItem; // src/components/Summary/Summary.tsx import SummaryItem from "./SummaryItem"; const Summary = () => { return ( <> <div className="flex justify-between"> <SummaryItem itemName={"Total"} itemValue={3} /> <SummaryItem itemName={"To do"} itemValue={2} /> <SummaryItem itemName={"Done"} itemValue={1} /> </div> </> ); }; export default Summary;


你会注意到 SummaryItem 有两个道具: itemName ,类型为字符串, itemValue ,类型number 。当 SummaryItem 组件在 Summary 组件内部使用时,这些道具将被传递,然后在 SummaryItem JSX 中呈现。


任务


同样,对于任务部分(最后一个),我们有一个呈现 TaskItem 组件的任务组件。


现在也有静态数据。我们稍后需要将任务名称状态作为道具传递给 TaskItem 组件,以使其可重用和动态。


 // src/components/Tasks/TaskItem.tsx const TaskItem = () => { return ( <div className="flex justify-between bg-white p-1 px-3 rounded-sm"> <div className="flex gap-2 items-center"> <input type="checkbox" /> Task name </div> <button className="bg-green-200 hover:bg-green-300 rounded-lg p-1 px-3"> Delete </button> </div> ); }; export default TaskItem; // src/components/Tasks/Tasks.tsx import TaskItem from "./TaskItem"; const Tasks = () => { return ( <div className="flex flex-col gap-2"> <TaskItem /> </div> ); }; export default Tasks;


输入


最后,输入组件是一个带有标签、文本输入和“添加任务”按钮的表单。现在它什么都不做,但我们很快就会改变它。


 // src/components/Input.tsx const InputContainer = () => { return ( <form action="" className="flex flex-col gap-4"> <div className="flex flex-col"> <label className="text-white">Enter your next task:</label> <input className="p-1 rounded-sm" /> </div> <button type="button" className="bg-green-100 rounded-lg hover:bg-green-200 p-1" > Add task </button> </form> ); }; export default InputContainer;

状态:添加交互性

要在 React 中添加交互性,我们需要将信息存储在组件的状态中。


但在此之前,我们需要考虑我们希望数据如何随时间变化。我们需要确定此数据的最小表示,并确定我们应该使用哪些组件来存储此状态。


状态的最小表示


状态应该包含使我们的应用程序具有交互性所需的所有信息——但仅此而已。如果我们可以从不同的值计算出一个值,我们应该只保留其中一个处于状态。这使得我们的代码不仅不那么冗长,而且也不太容易出现涉及矛盾状态值的错误。


在我们的示例中,我们可能认为我们需要跟踪总任务、未决任务和已完成任务的值。


但是要跟踪任务,只需要一个数组,其中包含代表每个任务及其状态(待处理或已完成)的对象。


 const tasks = [ { name: "task one", done: false, }, { name: "task two", done: true, }, ];


有了这些数据,我们总能在渲染时使用数组方法找到我们需要的所有其他信息。我们还避免了矛盾的可能性,例如总共有 4 个任务,但只有 1 个待处理和 1 个已完成的任务。


我们还需要表单中的状态(在输入组件中),以便我们可以使其具有交互性。


国家应该住在哪里


这样想:哪些组件需要访问我们要存储在状态中的数据?如果它是单个组件,则状态可以存在于该组件本身中。如果需要数据的组件不止一个,那么您应该找到这些组件的公共父组件。


在我们的示例中,控制 Input 组件所需的状态只需要在那里访问,因此它可以是该组件的本地状态。


 // src/components/Input.tsx import { useState } from "react"; const InputContainer = () => { const [newTask, setNewTask] = useState(""); // Initialize newTask and setNewTask return ( <form action="" className="flex flex-col gap-4"> <div className="flex flex-col"> <label className="text-white">Enter your next task:</label> <input className="p-1 rounded-sm" type="text" value={newTask} // Set the input value to newTask onChange={(e) => setNewTask(e.target.value)} // Set newTask to the input value whenever the user types something /> </div> <button type="submit" className="bg-green-100 rounded-lg hover:bg-green-200 p-1" > Add task </button> </form> ); }; export default InputContainer;


它所做的是在输入中显示我们的newTask值,并在输入发生变化时调用setNewTask函数(即,当用户键入内容时)。


我们不会立即在 UI 中看到任何更改,但这是必要的,因此我们可以控制输入并访问其值以便稍后使用它。


然而,跟踪任务的状态必须以不同的方式处理,因为它需要在 SummaryItem 组件(我们需要显示总数、待处理和已完成任务的数量)以及 TaskItem 组件(我们需要显示任务本身)。它需要处于相同状态,因为此信息必须始终保持同步。


让我们看一下我们的组件树(您可以为此使用React 开发工具)。



我们可以看到第一个公共父组件是App。所以这就是我们的任务状态将要存在的地方。


有了状态,剩下的就是将数据作为道具传递给需要使用它的组件。


(我们还不担心如何对父状态进行和持久化任何更改,那是下一部分。)


 // src/App.tsx import { useState } from "react"; import { v4 as uuidv4 } from "uuid"; import Container from "./components/Container"; import Input from "./components/Input"; import Summary from "./components/Summary/Summary"; import Tasks from "./components/Tasks/Tasks"; export interface Task { name: string; done: boolean; id: string; } const initialTasks = [ { name: "task one", done: false, id: uuidv4(), }, { name: "task two", done: true, id: uuidv4(), }, ]; function App() { const [tasks, setTasks] = useState<Task[]>(initialTasks); return ( <div className="flex justify-center m-5"> <div className="flex flex-col items-center"> <div className="border shadow p-10 flex flex-col gap-10 sm:w-[640px]"> <Container title={"Summary"}> <Summary tasks={tasks} /> </Container> <Container> <Input /> </Container> <Container title={"Tasks"}> <Tasks tasks={tasks} /> </Container> </div> </div> </div> ); } export default App;


在这里,我们使用虚拟数据 ( initialTasks ) 初始化任务值,以便我们可以在应用程序完成之前将其可视化。稍后我们可以将其更改为一个空数组,这样新用户在打开应用程序时就不会看到任何任务。


除了namedone属性之外,我们还向我们的任务对象添加了一个 id,因为很快就会需要它。


我们正在定义一个interface ,其中包含任务对象中的值类型,并将其传递给useState函数。在这种情况下这是必要的,因为当我们将tasks的初始值更改为空数组时,或者当我们将它作为 props 传递时,TypeScript 将无法推断它。


最后,请注意我们将任务作为道具传递给 Summary 和 Tasks 组件。这些组件将需要更改以适应这种情况。


 // src/components/Summary/Summary.tsx import { Task } from "../../App"; import SummaryItem from "./SummaryItem"; const Summary = ({ tasks }: { tasks: Task[] }) => { const total = tasks.length; const pending = tasks.filter((t) => t.done === false).length; const done = tasks.filter((t) => t.done === true).length; return ( <> <div className="flex flex-col gap-1 sm:flex-row sm:justify-between"> <SummaryItem itemName={"Total"} itemValue={total} /> <SummaryItem itemName={"To do"} itemValue={pending} /> <SummaryItem itemName={"Done"} itemValue={done} /> </div> </> ); }; export default Summary;


我们更新了 Summary 组件,现在它可以接受tasks作为 prop。我们还定义了值totalpendingdone ,它们将作为道具传递给 SummaryItem 组件,代替我们之前的静态itemValue


 // src/components/Tasks/Tasks.tsx import { Task } from "../../App"; import TaskItem from "./TaskItem"; const Tasks = ({ tasks }: { tasks: Task[] }) => { return ( <div className="flex flex-col gap-2"> {tasks.map((t) => ( <TaskItem key={t.id} name={t.name} /> ))} </div> ); }; export default Tasks; // src/components/Tasks/TaskItem.tsx import { useState } from "react"; const TaskItem = ({ name }: { name: string }) => { const [done, setDone] = useState(false); return ( <div className="flex justify-between bg-white p-1 px-3 rounded-sm gap-4"> <div className="flex gap-2 items-center"> <input type="checkbox" checked={done} onChange={() => setDone(!done)} /> {name} </div> <button className="bg-green-200 hover:bg-green-300 rounded-lg p-1 px-3"> Delete </button> </div> ); }; export default TaskItem;


对于 Tasks 组件,我们也将task作为 prop,将其name属性映射到 TaskItem 组件。结果,我们为tasks数组中的每个对象获得了一个 TaskItem 组件。我们还更新了 TaskItem 组件以接受name作为道具。


这就是 id 派上用场的地方,因为每次我们有一个子组件列表时,我们都需要传递一个唯一的键。如果我们不添加密钥,这可能会导致重新渲染时出现错误。 (在生产应用中,id 很可能来自后端。)


目前的结果是这样的:



我们已经可以看到反映我们的虚拟数据的摘要编号和任务名称。但是我们仍然缺少添加或删除任务的方法。


添加反向数据流


要完成我们的应用程序,我们需要一种方法来更改来自 Input 和 TaskItem 子组件的 App 组件状态(任务数据所在的位置)。


为此,我们可以使用useState钩子生成的函数来定义事件处理程序,并将它们作为 props 传递下去。一旦我们这样做了,我们只需在子组件的适当用户交互期间调用它们。


确保在更新状态时永远不要改变状态,因为这会导致错误。更新状态对象时总是用新对象替换状态对象。


下面是我们最终的 App 组件,其中声明了处理程序并将其作为 props 传递给 Input 和 Tasks 组件。


handleSubmit返回一个包含旧任务和新任务的新数组。 toggleDoneTask为指定的id返回一个具有相反done属性的新数组。 handleDeleteTask返回一个没有指定id任务的新数组。


 // src/App.tsx import { FormEvent, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import Container from "./components/Container"; import Input from "./components/Input"; import Summary from "./components/Summary/Summary"; import Tasks from "./components/Tasks/Tasks"; export interface Task { name: string; done: boolean; id: string; } function App() { const [tasks, setTasks] = useState<Task[]>([]); const handleSubmit = (e: FormEvent<HTMLFormElement>, value: string) => { e.preventDefault(); const newTask = { name: value, done: false, id: uuidv4(), }; setTasks((tasks) => [...tasks, newTask]); }; const toggleDoneTask = (id: string, done: boolean) => { setTasks((tasks) => tasks.map((t) => { if (t.id === id) { t.done = done; } return t; }) ); }; const handleDeleteTask = (id: string) => { setTasks((tasks) => tasks.filter((t) => t.id !== id)); }; return ( <div className="flex justify-center m-5"> <div className="flex flex-col items-center"> <div className="border shadow p-10 flex flex-col gap-10 sm:w-[640px]"> <Container title={"Summary"}> <Summary tasks={tasks} /> </Container> <Container> <Input handleSubmit={handleSubmit} /> </Container> <Container title={"Tasks"}> <Tasks tasks={tasks} toggleDone={toggleDoneTask} handleDelete={handleDeleteTask} /> </Container> </div> </div> </div> ); } export default App;


这是使用handleSubmit更新 App 组件状态的最终 Input 组件。


 // src/components/Input.tsx import { FormEvent, useState } from "react"; const InputContainer = ({ handleSubmit, }: { handleSubmit: (e: FormEvent<HTMLFormElement>, value: string) => void; }) => { const [newTaskName, setNewTaskName] = useState(""); return ( <form action="" className="flex flex-col gap-4" onSubmit={(e) => { handleSubmit(e, newTaskName); setNewTaskName(""); }} > <div className="flex flex-col"> <label className="text-white">Enter your next task:</label> <input className="p-1 rounded-sm" type="text" value={newTaskName} onChange={(e) => setNewTaskName(e.target.value)} /> </div> <button type="submit" className="bg-green-100 rounded-lg hover:bg-green-200 p-1" > Add task </button> </form> ); }; export default InputContainer;


这是最终的 Tasks 组件,我们对其进行更新以将 props 从 App 传递到 TaskItem。我们还添加了一个三元运算符来返回“还没有任务!”没有任务的时候。


 // src/components/Tasks/Tasks.tsx import { Task } from "../../App"; import TaskItem from "./TaskItem"; const Tasks = ({ tasks, toggleDone, handleDelete, }: { tasks: Task[]; toggleDone: (id: string, done: boolean) => void; handleDelete: (id: string) => void; }) => { return ( <div className="flex flex-col gap-2"> {tasks.length ? ( tasks.map((t) => ( <TaskItem key={t.id} name={t.name} done={t.done} id={t.id} toggleDone={toggleDone} handleDelete={handleDelete} /> )) ) : ( <span className="text-green-100">No tasks yet!</span> )} </div> ); }; export default Tasks;


这是最终的 TaskItem 组件,使用toggleDonehandleDelete更新 App 组件状态。


 // src/components/Tasks/TaskItem.tsx const TaskItem = ({ name, done, id, toggleDone, handleDelete, }: { name: string; done: boolean; id: string; toggleDone: (id: string, done: boolean) => void; handleDelete: (id: string) => void; }) => { return ( <div className="flex justify-between bg-white p-1 px-3 rounded-sm gap-4"> <div className="flex gap-2 items-center"> <input type="checkbox" checked={done} onChange={() => toggleDone(id, !done)} /> {name} </div> <button className="bg-green-200 hover:bg-green-300 rounded-lg p-1 px-3" type="button" onClick={() => handleDelete(id)} > Delete </button> </div> ); }; export default TaskItem;


这是我们添加一些任务后的最终应用程序!



如果您正在编写代码,则可以按照这些说明部署您自己的应用程序。

您可以在此处找到包含我们经过的所有代码的存储库,以及在此处找到该应用程序的实时版本。

最后的话

总之,构建一个待办事项列表应用程序是学习和巩固我们对 React 及其原则的理解的好方法。通过将流程分解为小步骤并遵循最佳实践,我们可以在相对较短的时间内创建功能强大的应用程序。


我们涵盖了:


  • 组件、状态和逆向数据流的关键概念。

  • 应用程序的设计和架构。

  • 单一职责原则等最佳实践


通过遵循本指南中概述的步骤,您现在应该对如何构建简单的 React 应用程序并能够将其应用到您自己的项目有一个扎实的了解。


编码愉快!