<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ React - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ React - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 21 May 2026 15:49:23 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/react/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ React 19 的新功能 ]]>
                </title>
                <description>
                    <![CDATA[ React 19 刚刚发布，为这个流行的 JavaScript 库带来了大量新功能和增强功能。对于希望保持领先地位的开发人员来说，了解这些更新至关重要。本课程旨在帮助你了解 React 19 的最新变化，从数据变更处理到有助于改善用户体验的创新型新 API。 我们刚刚在 freeCodeCamp.org [https://www.freecodecamp.org/] YouTube 频道上发布了一门课程，该课程将向你介绍 React 19 中令人兴奋的新功能。其中包括处理 Actions、乐观更新、表单状态、新的 use() API 等的内置方法。React 19 中包含大量改进，可简化开发流程并提高应用程序性能，对于 React 开发者来说是重大的更新。 来自 Scrimba 的 Bob Ziroll 开发了本课程。他是世界上最受欢迎的 React 讲师之一。 自 2022 年 React 18 发布以来，React 已经两年多没有重大版本更新了。在 React 19 中，有许多特性可以简化对数据变更的处理，同时提供 API 通过乐观更新来增强用户体验。这些更新旨在使应用程序更 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/whats-new-in-react-19/</link>
                <guid isPermaLink="false">6684cfc49100b5049d88a718</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Devan Wang ]]>
                </dc:creator>
                <pubDate>Wed, 03 Jul 2024 04:20:13 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/07/800a24a8-98cd-4979-b062-9ff4cd1c35ad.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/whats-new-in-react-19/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">What's New in React 19</a>
      </p><!--kg-card-begin: markdown--><p>React 19 刚刚发布，为这个流行的 JavaScript 库带来了大量新功能和增强功能。对于希望保持领先地位的开发人员来说，了解这些更新至关重要。本课程旨在帮助你了解 React 19 的最新变化，从数据变更处理到有助于改善用户体验的创新型新 API。</p>
<p>我们刚刚在 <a href="https://www.freecodecamp.org/">freeCodeCamp.org</a> YouTube 频道上发布了一门课程，该课程将向你介绍 React 19 中令人兴奋的新功能。其中包括处理 Actions、乐观更新、表单状态、新的 <code>use()</code> API 等的内置方法。React 19 中包含大量改进，可简化开发流程并提高应用程序性能，对于 React 开发者来说是重大的更新。</p>
<p>来自 Scrimba 的 Bob Ziroll 开发了本课程。他是世界上最受欢迎的 React 讲师之一。</p>
<p>自 2022 年 React 18 发布以来，React 已经两年多没有重大版本更新了。在 React 19 中，有许多特性可以简化对数据变更的处理，同时提供 API 通过乐观更新来增强用户体验。这些更新旨在使应用程序更快、反应更灵敏，即使在执行复杂的数据操作时也是如此。此外，React 团队还发布了新的开源编译器，他们已经开发了多年，旨在管理许多与性能相关的幕后细节，使开发者能够更多地专注于构建功能，而不是优化。</p>
<h3 id="">课程概览</h3>
<p>参与这个综合课程，我们将深入研究 React 19 最新和最强大的功能。本课程包括对 React 18 中重要概念的复习，以及对 React 19 中新功能的深入介绍。以下是你将学习的内容：</p>
<h4 id="transitionreact18">Transition（复习 React 18）</h4>
<p>我们先快速回顾一下 Transition，这是 React 18 中引入的一个关键特性，有助于更顺畅地管理 UI 更新。</p>
<h4 id="react">React 编译器</h4>
<p>了解新的 React 编译器，这是一个强大的工具，可自动处理性能优化，让开发者编写出更简洁、更高效的代码。</p>
<h4 id="actions">表单 Actions</h4>
<p>探索新的表单 Actions 特性，简化表单状态管理，更有效地处理用户交互。</p>
<h4 id="useactionstate"><code>useActionState()</code></h4>
<p>了解如何使用 <code>useActionState()</code> 这个新 Hook，以更直观的方式管理异步 Actions 的状态。这部分内容分为三个部分进行深入探讨。</p>
<h4 id="actions">Actions 中的错误处理</h4>
<p>掌握 Actions 中改进的错误处理方式，以创建更强大的应用程序。</p>
<h4 id="useoptimistic"><code>useOptimistic()</code></h4>
<p>了解 <code>useOptimistic()</code> 这个 Hook 如何帮助你乐观更新用户界面，在等待服务器确认期间立即呈现更改，提供更好的用户体验。</p>
<h4 id="useformstatus"><code>useFormStatus</code></h4>
<p>了解如何使用 <code>useFormStatus</code> 这个新 Hook 来更有效地追踪表单状态。</p>
<h4 id="refprop">ref 作为 prop</h4>
<p>了解将 ref 用作 prop 的新方法，这种方法增强了组件的交互和操作。</p>
<h4 id="use"><code>use()</code></h4>
<p>深入了解 <code>use()</code> API 这个新增功能，它进一步简化了 React 应用中的对异步代码的管理。</p>
<h4 id="">其他改进</h4>
<p>我们还将介绍其他一些改进，例如增加了对元数据标签的支持，从而带来更流畅、更高效的开发体验。</p>
<h3 id="">课程目录</h3>
<ul>
<li>
<p>React 19 有什么新特性？</p>
</li>
<li>
<p>React 18 回顾 - useTransition (1)</p>
</li>
<li>
<p>React 18 回顾 - useTransition (2)</p>
</li>
<li>
<p>React 编译器</p>
</li>
<li>
<p>表单 Actions</p>
</li>
<li>
<p>React 中的错误和加载状态</p>
</li>
<li>
<p>useActionState() - 第 1 部分</p>
</li>
<li>
<p>useActionState() - 第 2 部分</p>
</li>
<li>
<p>useActionState() - 第 3 部分</p>
</li>
<li>
<p>useOptimistic()</p>
</li>
<li>
<p>useFormStatus()</p>
</li>
<li>
<p>ref 作为 prop</p>
</li>
<li>
<p>use()</p>
</li>
<li>
<p>其他改进 - 元数据标签</p>
</li>
<li>
<p>总结</p>
</li>
</ul>
<p>无论你是经验丰富的 React 开发者还是刚刚入门，本课程都能提供充分利用 React 19 潜力所需的知识技能。在 <a href="https://www.youtube.com/watch?v=81uAxzeyL2I&amp;feature=youtu.be">freeCodeCamp.org YouTube 频道</a>上观看完整课程（总共 1 小时）。</p>
<figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.25%;" class="fluid-width-video-wrapper">
            <iframe width="560" height="315" src="https://www.youtube.com/embed/81uAxzeyL2I" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy" name="fitvid0"></iframe>
          </div>
        </div>
      </figure>
<!--kg-card-end: markdown--><p>你也可以<a href="https://www.bilibili.com/video/BV13H4y1w7o5/?spm_id_from=333.999.0.0">在 bilibili 观看这个视频</a>。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ React 开发人员的 TypeScript 手册 —— 如何构建类型安全的 Todo 应用程序 ]]>
                </title>
                <description>
                    <![CDATA[ 在当今的JavaScript生态中，TypeScript越来越受欢迎。越来越多的React开发者开始使用它。 如果你是React开发者，希望探索TypeScript或提升自己的技能，这本手册正适合你。我将指导你通过构建一个经典的待办事项应用，来在React应用中使用TypeScript。 我将涵盖作为一个React开发者开始使用TypeScript所需知道的一切。你将学会如何使用强类型处理状态和属性，如何用TypeScript创建React组件，如何在React Hooks中使用TypeScript，以及如何与Context API一起使用TypeScript。 通过本教程的学习，你将对TypeScript有一个坚实的理解，并准备好自信地开发类型安全的React应用程序。所以，不用再等待，让我们开始吧！ 我们将涵盖以下内容  * 先决条件  * 我们将要构建什么  * 如何开始  * 如何设置待办事项应用的组件  * 如何在React中创建一个简单的表单元素  * TypeScript中的类型错误是什么以及如何修复它  * TypeScript中的泛型是什么  * 如何在Rea ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/typescript-tutorial-for-react-developers/</link>
                <guid isPermaLink="false">65dc09724985d903ee575e18</guid>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ YiWei ]]>
                </dc:creator>
                <pubDate>Mon, 26 Feb 2024 04:17:04 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/02/TypeScript-Handbook-for-React-Developers-Cover.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/typescript-tutorial-for-react-developers/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">TypeScript Handbook for React Developers – How to Build a Type-Safe Todo App</a>
      </p><!--kg-card-begin: markdown--><p>在当今的JavaScript生态中，TypeScript越来越受欢迎。越来越多的React开发者开始使用它。</p>
<p>如果你是React开发者，希望探索TypeScript或提升自己的技能，这本手册正适合你。我将指导你通过构建一个经典的待办事项应用，来在React应用中使用TypeScript。</p>
<p>我将涵盖作为一个React开发者开始使用TypeScript所需知道的一切。你将学会如何使用强类型处理状态和属性，如何用TypeScript创建React组件，如何在React Hooks中使用TypeScript，以及如何与Context API一起使用TypeScript。</p>
<p>通过本教程的学习，你将对TypeScript有一个坚实的理解，并准备好自信地开发类型安全的React应用程序。所以，不用再等待，让我们开始吧！</p>
<h2 id="">我们将涵盖以下内容</h2>
<ul>
<li><a href="#prerequisites">先决条件</a></li>
<li><a href="#what-are-we-going-to-build">我们将要构建什么</a></li>
<li><a href="#getting-started">如何开始</a></li>
<li><a href="#how-to-set-up-the-todo-app-component">如何设置待办事项应用的组件</a></li>
<li><a href="#how-to-create-a-simple-form-element-in-react">如何在React中创建一个简单的表单元素</a></li>
<li><a href="#what-is-a-type-error-in-typescript-and-how-to-fix-it">TypeScript中的类型错误是什么以及如何修复它</a></li>
<li><a href="#what-are-the-generic-types-in-typescript">TypeScript中的泛型是什么</a></li>
<li><a href="#how-to-handle-form-submission-with-typescript-in-react">如何在React中用TypeScript处理表单提交</a></li>
<li><a href="#how-to-automatically-focus-on-an-input-field-in-react">如何在React中自动聚焦一个输入字段</a></li>
<li><a href="#what-is-useref-and-how-to-to-use-it-with-typescript">什么是<code>useRef</code>以及如何在TypeScript中使用它</a></li>
<li><a href="#how-to-create-type-safe-react-components-with-typescript">如何用TypeScript创建类型安全的React组件</a></li>
<li><a href="#what-is-forwardref-in-react">React中的<code>forwardRef</code>是什么</a></li>
<li><a href="#how-to-create-a-todo-item-on-the-form-submission">如何在表单提交时创建一个待办事项</a></li>
<li><a href="#what-is-react-context">什么是React上下文</a></li>
<li><a href="#how-to-use-react-context-with-typescript">如何在TypeScript中使用React上下文</a></li>
<li><a href="#what-are-interfaces-in-typescript">TypeScript中的接口是什么</a></li>
<li><a href="#how-to-use-typescript-interfaces-with-react-context">如何将TypeScript接口与React上下文一起使用</a></li>
<li><a href="#how-to-create-a-custom-hook-to-consume-react-context">如何创建一个自定义钩子来使用React上下文</a></li>
<li><a href="#how-to-define-an-interface-for-todo-items">如何为待办事项定义一个接口</a></li>
<li><a href="#how-to-build-a-custom-react-component-for-displaying-todo-items">如何构建一个自定义的React组件来展示待办事项</a></li>
<li><a href="#how-to-implement-functionality-edit-delete-and-update-todo-items">如何实现功能：编辑、删除和更新待办事项</a></li>
<li><a href="#conclusion">结论</a></li>
</ul>
<h2 id="prerequisites">先决条件</h2>
<p>开始本教程无需事先了解TypeScript，使其非常适合初学者。然而，拥有React的背景知识将极大地增强你的理解力，并在整个教程中最大限度地提升你的学习潜力。</p>
<p>在本教程中，你将使用以下工具：</p>
<ol>
<li><strong>React 18.2.0：</strong> React是一个用于构建用户界面的JavaScript库。它允许开发者创建可重用的UI组件，并根据数据变化高效地更新UI。</li>
<li><strong>TypeScript：</strong> TypeScript是JavaScript的一种静态类型超集，增加了可选的类型注释。它提供了增强的工具，并帮助在开发过程中捕获潜在的错误，使代码更可靠，更易于维护。</li>
<li><strong>Vite：</strong> Vite是一个用于现代Web应用的快速开发服务器和构建工具。它提供即时服务器启动、热模块替换和优化的构建输出，使开发流程快速而高效。</li>
<li><strong>Framer Motion：</strong> Framer Motion是React的一种流行动画库。它提供了一个易于使用的界面，用于在Web应用中创建流畅的互动动画和过渡，增强了整体用户体验。</li>
</ol>
<p>在接下来的部分中，你将对你将在本教程中构建的项目有一个简洁的预览。</p>
<h2 id="what-are-we-going-to-build">我们将要构建什么</h2>
<p>我们将要构建一个经典的待办事项应用程序。它将具有以下功能：</p>
<ul>
<li>添加一个待办事项。</li>
<li>编辑一个待办事项。</li>
<li>删除一个待办事项。</li>
<li>标记一个待办事项是否完成。</li>
<li>在浏览器的本地存储中存储待办事项。</li>
<li>当用户尝试添加或编辑一个空标题的待办事项时，显示适当的错误消息。</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/ezgif-3-98866e5ad0.gif" alt="This is a todo app where users can add or delete an item, also they can edit an existing item or mark them as completed" width="600" height="400" loading="lazy"></p>
<p>上图是应用程序最终预览。</p>
<h2 id="getting-started">如何开始</h2>
<p>为了开始本教程，我已经为你准备了一个包含所有必需依赖项的样板项目。这消除了从头开始设置项目的需要。</p>
<p>只需从GitHub仓库克隆<a href="https://github.com/Yazdun/react-ts-fcc-tutorial/tree/starter">起始样板</a>，然后跟随教程。这样，你可以专注于学习和实现概念，而不会被设置细节所困扰。</p>
<ul>
<li>起始样板：<a href="https://github.com/Yazdun/react-ts-fcc-tutorial/tree/starter">在GitHub上查看</a></li>
<li>最终版本：<a href="https://github.com/Yazdun/react-ts-fcc-tutorial">在GitHub上查看</a></li>
</ul>
<p>一旦你设置好起始样板并成功地在你的本地机器上运行它，你应该能够看到初始页面。这个页面将作为我们旅程的起点。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/06/image-314.png" alt="简单的页面，显示着“待办事项应用”的文字。这个页面作为我们教程的起点" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>起始样板</figcaption>
</figure>
<p>现在，我们将开始为我们的应用添加令人兴奋的功能。让我们立即开始吧！</p>
<h2 id="how-to-set-up-the-todo-app-component">如何设置待办事项应用的组件</h2>
<p>在这一部分，你将设置你的待办事项应用的主要组件，并逐渐增强它的附加功能。打开<code>./src/App.tsx</code>并添加以下代码：</p>
<pre><code class="language-tsx">//📂./src/App.tsx

import { TodoList, AddTodo } from './components'
import { Toaster } from 'react-hot-toast'

function App() {
  return (
    &lt;div&gt;
      &lt;Toaster position="bottom-center" /&gt;
      &lt;AddTodo /&gt;
      &lt;TodoList /&gt;
    &lt;/div&gt;
  )
}

export default App
</code></pre>
<p>让我们一步步分解：</p>
<ul>
<li><code>&lt;Toaster position="bottom-center" /&gt;</code>：这个组件负责在屏幕底部中央显示toast通知。</li>
<li><code>&lt;AddTodo /&gt;</code>：这个组件将表示一个输入字段和一个按钮，用于向应用添加新的待办事项。</li>
<li><code>&lt;TodoList /&gt;</code>：这个组件将渲染现有待办事项的列表。</li>
</ul>
<p>现在，在你的浏览器上打开你的本地服务器，你将能看到以下页面：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/06/image-315.png" alt="App.tsx的预览" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>App.tsx的预览</figcaption>
</figure>
<p>这两个组件在你的应用中起着至关重要的作用。在接下来的部分中，你将构建使用<code>&lt;AddTodo /&gt;</code>组件添加待办事项的功能。具体来说，你将学习如何在React中使用TypeScript处理表单提交。</p>
<h2 id="how-to-create-a-simple-form-element-in-react">如何在React中创建一个简单的表单元素</h2>
<p>首先，你需要为创建一个待办事项创建一个表单元素。为了在你的应用中实现这一点，你需要创建一个表单并有效地处理表单提交。在这一部分中，你将探索如何在React应用中使用TypeScript处理表单提交。</p>
<p>我只是想给你一个快速提示，因为你即将遇到你在TypeScript中的第一个类型错误！将以下代码添加到<code>components/AddTodo.tsx</code>：</p>
<pre><code class="language-tsx">//📂./src/components/AddTodo.tsx
//⚠️TypeScript is not happy with this code

import React, { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { useTodo } from '../context'
import { Input } from './Input'

export const AddTodo = () =&gt; {
  const [input, setInput] = useState()

  return (
    &lt;form&gt;
      &lt;div className="flex items-center w-full max-w-lg gap-2 p-5 m-auto"&gt;
        &lt;input
          value={input}
          onChange={e =&gt; setInput(e.target.value)}
          type="text"
          className="w-full px-5 py-2 bg-transparent border-2 outline-none border-zinc-600 rounded-xl placeholder:text-zinc-500 focus:border-white"
          placeholder="start typing ..."
        /&gt;
        &lt;button
          type="submit"
          className="px-5 py-2 text-sm font-normal text-blue-300 bg-blue-900 border-2 border-blue-900 active:scale-95 rounded-xl"
        &gt;
          Submit
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  )
}
</code></pre>
<p>你创建了一个useState钩子，它会随着输入值的改变而更新状态。然而，TypeScript对这段代码不满意。但为什么TypeScript会不满意呢？</p>
<h3 id="what-is-a-type-error-in-typescript-and-how-to-fix-it">TypeScript中的类型错误是什么以及如何修复它</h3>
<p>TypeScript中的类型定义了变量可以持有的数据种类，并在开发过程中启用了错误和漏洞的检测。</p>
<p>当一个值以与其预期类型不兼容的方式使用时，就会在TypeScript中出现类型错误，导致代码中可能出现漏洞或意外行为。</p>
<p>在我们的案例中，TypeScript显示这段代码有错误，因为它无法自动推断状态变量<code>input</code>的类型。要解决这个问题，你需要明确地提供TypeScript类型信息。在这种情况下，你希望input是字符串类型，因为它代表输入字段的值。</p>
<p>要修复这个错误，你有两个选择。简单的解决方案是向<code>useState</code>钩子添加一个初始值，TypeScript将自动推断<code>input</code>类型为字符串：</p>
<pre><code class="language-tsx"> const [input, setInput] = useState('')
</code></pre>
<p>通过添加上述代码，你可能会注意到错误消失了，TypeScript也满意了。但并不是所有的错误都能在TypeScript中这么容易解决。</p>
<p>让我们考虑一个情况，你对你的状态的类型不确定，不能确定它应该初始化为数字还是字符串。这种不确定性引导我们使用第二个选项，即使用泛型。</p>
<h3 id="what-are-the-generic-types-in-typescript">TypeScript中的泛型是什么</h3>
<p>泛型提供了一种处理你不确定特定值类型的情况的方法。通过泛型，你可以定义一个占位符来代表实际的类型，使你的代码更加灵活和可重用：</p>
<pre><code class="language-tsx">const [state, setState] = useState&lt;string | number&gt;('')
</code></pre>
<p>上述代码初始化了一个名为“state”的状态变量，其初始值为空字符串，但它允许状态变量存储字符串或数字作为其值。</p>
<p>现在，让我们在你的应用中引入一个泛型。我们不希望你的用户添加数字作为待办事项 - 我们希望他们只能添加字符串：</p>
<pre><code class="language-tsx">//📂./src/components/AddTodo.tsx
//✅TypeScript is happy with this code

import React, { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { useTodo } from '../context'
import { Input } from './Input'

export const AddTodo = () =&gt; {
  const [input, setInput] = useState&lt;string&gt;('')

  return (
    &lt;form&gt;
      &lt;div className="flex items-center w-full max-w-lg gap-2 p-5 m-auto"&gt;
        &lt;input
          value={input}
          onChange={e =&gt; setInput(e.target.value)}
          type="text"
          className="w-full px-5 py-2 bg-transparent border-2 outline-none border-zinc-600 rounded-xl placeholder:text-zinc-500 focus:border-white"
          placeholder="start typing ..."
        /&gt;
        &lt;button
          type="submit"
          className="px-5 py-2 text-sm font-normal text-blue-300 bg-blue-900 border-2 border-blue-900 active:scale-95 rounded-xl"
        &gt;
          Submit
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  )
}
</code></pre>
<p>通过在<code>useState</code>函数后指定<code>&lt;string&gt;</code>，我们确保状态变量<code>input</code>只能持有字符串类型的值。这样可以防止用户输入数字或任何其他不兼容的数据类型作为待办事项。</p>
<h3 id="how-to-handle-form-submission-with-typescript-in-react">如何在React中使用TypeScript处理表单提交</h3>
<p>既然你已经成功地将输入值存储在状态中，让我们继续处理表单提交本身：</p>
<pre><code class="language-tsx">//📂./src/components/AddTodo.tsx

import React, { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { useTodo } from '../context'
import { Input } from './Input'

export const AddTodo = () =&gt; {
  const [input, setInput] = useState&lt;string&gt;('')

  const handleSubmission = (e: React.FormEvent) =&gt; {
    e.preventDefault()
    console.log('form has been submitted')
  }

  return (
    &lt;form onSubmit={handleSubmission}&gt;
      &lt;div className="flex items-center w-full max-w-lg gap-2 p-5 m-auto"&gt;
        &lt;input
          value={input}
          onChange={e =&gt; setInput(e.target.value)}
          type="text"
          className="w-full px-5 py-2 bg-transparent border-2 outline-none border-zinc-600 rounded-xl placeholder:text-zinc-500 focus:border-white"
          placeholder="start typing ..."
        /&gt;
        &lt;button
          type="submit"
          className="px-5 py-2 text-sm font-normal text-blue-300 bg-blue-900 border-2 border-blue-900 active:scale-95 rounded-xl"
        &gt;
          Submit
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  )
}

</code></pre>
<p>当表单被提交时，会调用<code>handleSubmission</code>函数。让我们逐步分解它：</p>
<ol>
<li><code>(e: React.FormEvent)</code>是函数的参数声明。它指定函数期望传递一个类型为<code>React.FormEvent</code>的事件对象作为参数。<code>React.FormEvent</code>是表示在表单元素上发生的事件的事件对象类型，例如提交表单或与表单字段互动。</li>
<li><code>e.preventDefault()</code>是属于事件对象（<code>e</code>）的方法。它被调用以阻止表单提交的默认行为，即刷新页面。通过调用<code>preventDefault()</code>，我们覆盖了默认行为并阻止了页面刷新。</li>
<li><code>console.log('form has been submitted')</code>是一个简单的语句，将消息记录到浏览器的控制台。在这种情况下，它在表单提交事件发生时记录消息“form has been submitted”。</li>
</ol>
<p>太好了！你已经完成了处理表单提交所需的步骤。现在让我们继续到下一部分，在那里你将通过做一些修改来增强你的表单功能。</p>
<h3 id="how-to-automatically-focus-on-an-input-field-in-react">如何在React中自动聚焦输入字段</h3>
<p>为了提升用户体验，你可以在应用最初加载时自动将焦点设置在“添加待办事项”的输入字段上。这消除了用户在打开应用时手动点击输入框的需要。</p>
<p>为了实现这个功能，你可以使用一个特定的React钩子，称为<code>useRef</code>，它允许你将这个特性整合到输入框中。</p>
<h4 id="what-is-useref-and-how-to-to-use-it-with-typescript">什么是`useRef`以及如何在TypeScript中使用它</h4>
<p><code>useRef</code>是React中的一个特殊钩子，用于在组件中创建对一个元素或值的引用。这个引用可以用来直接访问和操作被引用的元素，而不会导致重新渲染。</p>
<p>你通常会用它来访问DOM元素、管理焦点或在组件渲染中存储可变值。</p>
<p>打开应用<code>components/AddTodo.tsx</code>并添加以下代码：</p>
<pre><code class="language-tsx">//📂./src/components/AddTodo.tsx

import React, { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { useTodo } from '../context'
import { Input } from './Input'

export const AddTodo = () =&gt; {
  const [input, setInput] = useState&lt;string&gt;('')
  const inputRef = useRef&lt;HTMLInputElement&gt;(null)

  useEffect(() =&gt; {
    if (inputRef.current) {
      inputRef.current.focus()
    }
  }, [])

  const handleSubmission = (e: React.FormEvent) =&gt; {
    e.preventDefault()
    console.log('form has been submitted')
  }

  return (
    &lt;form onSubmit={handleSubmission}&gt;
      &lt;div className="flex items-center w-full max-w-lg gap-2 p-5 m-auto"&gt;
        &lt;input
          ref={inputRef}
          value={input}
          onChange={e =&gt; setInput(e.target.value)}
          type="text"
          className="w-full px-5 py-2 bg-transparent border-2 outline-none border-zinc-600 rounded-xl placeholder:text-zinc-500 focus:border-white"
          placeholder="start typing ..."
        /&gt;
        &lt;button
          type="submit"
          className="px-5 py-2 text-sm font-normal text-blue-300 bg-blue-900 border-2 border-blue-900 active:scale-95 rounded-xl"
        &gt;
          Submit
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  )
}
</code></pre>
<p>这里，React的<code>useRef</code>钩子与TypeScript一起使用。</p>
<ul>
<li>行<code>const inputRef = useRef&lt;HTMLInputElement&gt;(null)</code>使用useRef钩子声明了一个名为<code>inputRef</code>的引用变量。类型参数<code>&lt;HTMLInputElement&gt;</code>指定该ref用于输入元素。ref的初始值设置为<code>null</code>。</li>
<li>在useEffect钩子中，检查<code>inputRef.current</code>是否存在。如果存在，则调用其上的<code>focus()</code>方法，这意味着当组件被装载时，输入字段将接收焦点。</li>
</ul>
<p><code>useRef</code>钩子使用<code>&lt;HTMLInputElement&gt;</code>进行类型参数化，以确保引用与输入元素兼容。</p>
<p>通过结合使用useRef和TypeScript，代码不仅受益于TypeScript的静态类型检查，还能使用useRef与输入元素的DOM引用进行交互。</p>
<p>虽然这段代码可以正确运行，但将此输入组件在应用的其他部分复用将是有益的。因此，让我们创建一个可复用的输入组件，并探索如何通过实现这个输入来开发类型安全的React组件。</p>
<h3 id="how-to-create-type-safe-react-components-with-typescript">如何用TypeScript创建类型安全的React组件</h3>
<p>在这一部分中，你将为应用中未来的使用案例创建一个类型安全的Input组件。</p>
<p>为了创建这个自定义的Input组件，你需要将在上一节中创建的ref作为prop传递给这个组件。</p>
<p>Refs作为普通的props传递，为了将refs传递给子组件，你需要实现一个名为forwardRef的特殊内置React函数。</p>
<h4 id="what-is-forwardref-in-react">React中的`forwardRef`是什么</h4>
<p>在React中，<code>forwardRef</code>函数是一个特性，它允许你从父组件向子组件传递ref。Refs用于直接访问和操作底层的DOM元素。</p>
<p>通过使用<code>forwardRef</code>，你可以创建一个自定义组件，该组件可以接收一个ref，并将其传递到组件内的特定元素。</p>
<p>这使得父组件能够与子组件的底层元素进行交互，例如聚焦输入字段或触发某些动作。</p>
<p>简而言之，<code>forwardRef</code>帮助你在组件之间连接ref，使你在需要时能够控制或访问子组件的内部元素。</p>
<p>现在，让我们创建一个可复用的Input组件。打开<code>components/Input.tsx</code>：</p>
<pre><code class="language-tsx">// 📂./src/components/Input.tsx

import { InputHTMLAttributes, forwardRef } from 'react'
import cn from 'classnames'

export const Input = forwardRef&lt;
  HTMLInputElement,
  InputHTMLAttributes&lt;HTMLInputElement&gt;
&gt;(({ className, ...rest }, ref) =&gt; {
  return (
    &lt;input
      {...rest}
      ref={ref}
      className={cn(
        'w-full px-5 py-2 bg-transparent border-2 outline-none border-zinc-600 rounded-xl placeholder:text-zinc-500 focus:border-white',
        className,
      )}
    /&gt;
  )
})
</code></pre>
<p>让我们逐步分解这个组件：</p>
<ol>
<li>该组件使用React中的<code>forwardRef</code>函数将ref传递到底层的<code>&lt;input&gt;</code>元素。这允许父组件直接访问和操作输入元素。</li>
<li><code>HTMLInputElement</code>指定了将被传递到底层<code>&lt;input&gt;</code>元素的ref的类型。这确保了ref与输入元素期望的类型兼容。</li>
<li><code>InputHTMLAttributes&lt;HTMLInputElement&gt;</code>指定了组件接受的props对象的类型。这包括所有标准的HTML输入元素属性，例如<code>value</code>、<code>placeholder</code>、<code>onChange</code>等。</li>
<li>该组件从<code>rest</code>对象中解构出<code>className</code>属性，并且接收<code>ref</code>作为参数。</li>
<li>在组件内部，使用JSX表达式来渲染一个<code>&lt;input&gt;</code>元素。扩展运算符（<code>{...rest}</code>）被用于将组件接收到的所有props（除了<code>className</code>和<code>ref</code>）传递给<code>&lt;input&gt;</code>元素。这确保传递给<code>&lt;Input&gt;</code>组件的任何额外属性都将应用于底层的<code>&lt;input&gt;</code>元素。</li>
<li>使用<code>ref</code>属性将<code>ref</code>分配给底层的<code>&lt;input&gt;</code>元素，使得父组件能够引用输入元素。</li>
<li><code>className</code>是通过<code>classnames</code>模块中的<code>cn</code>函数构建的。这个函数基于提供的条件组合多个CSS类名。在这种情况下，它将默认输入元素的类名与传递给<code>&lt;Input&gt;</code>组件的<code>className</code>属性结合起来。</li>
</ol>
<p>最终渲染的<code>&lt;input&gt;</code>元素将具有组合的类名，并继承传递给<code>&lt;Input&gt;</code>组件的所有其他属性。</p>
<p>现在，让我们更新<code>&lt;AddTodo /&gt;</code>组件，以使用自定义的<code>&lt;Input /&gt;</code>替代默认的HTML输入元素：</p>
<pre><code class="language-tsx">//📂./src/components/AddTodo.tsx

import React, { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { useTodo } from '../context'
import { Input } from './Input'

export const AddTodo = () =&gt; {
  const [input, setInput] = useState&lt;string&gt;('')
  const inputRef = useRef&lt;HTMLInputElement&gt;(null)

  useEffect(() =&gt; {
    if (inputRef.current) {
      inputRef.current.focus()
    }
  }, [])

  const handleSubmission = (e: React.FormEvent) =&gt; {
    e.preventDefault()
    console.log('form has been submitted')
  }

  return (
    &lt;form onSubmit={handleSubmission}&gt;
      &lt;div className="flex items-center w-full max-w-lg gap-2 p-5 m-auto"&gt;
        &lt;Input
          ref={inputRef}
          value={input}
          onChange={e =&gt; setInput(e.target.value)}
          type="text"
          className="w-full px-5 py-2 bg-transparent border-2 outline-none border-zinc-600 rounded-xl placeholder:text-zinc-500 focus:border-white"
          placeholder="start typing ..."
        /&gt;
        &lt;button
          type="submit"
          className="px-5 py-2 text-sm font-normal text-blue-300 bg-blue-900 border-2 border-blue-900 active:scale-95 rounded-xl"
        &gt;
          Submit
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  )
}
</code></pre>
<p>现在，你可以在整个应用程序中使用这个自定义的<code>&lt;Input /&gt;</code>组件。在下一部分中，你将创建在表单提交时添加待办事项的功能。</p>
<h3 id="how-to-create-a-todo-item-on-the-form-submission">如何在表单提交时创建一个待办事项</h3>
<p>为了存储每个待办事项，你可以使用一个数组来保存用户的输入。本质上，我们需要一个字符串数组来存储每个待办事项：</p>
<pre><code class="language-tsx">const [todos, setTodos] = useState&lt;string[]&gt;([])
</code></pre>
<p><code>string[]</code>指定了将存储在<code>todos</code>状态变量中的数据类型。在这种情况下，它是一个字符串数组，意味着它将保存一个待办事项列表，其中每个项都表示为一个字符串。</p>
<p>现在让我们在表单提交时向<code>todos</code>中添加一个项：</p>
<pre><code class="language-tsx">//📂./src/components/AddTodo.tsx

import React, { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { useTodo } from '../context'
import { Input } from './Input'

export const AddTodo = () =&gt; {
  const [input, setInput] = useState&lt;string&gt;('')
  const [todos, setTodos] = useState&lt;string[]&gt;([])

  const handleSubmission = (e: React.FormEvent) =&gt; {
    e.preventDefault()
    if (input.trim() !== '') {
      setTodos([...todos, input])
      setInput('')
    }
  }

  return (
    &lt;form onSubmit={handleSubmission}&gt;
      &lt;div className="flex items-center w-full max-w-lg gap-2 p-5 m-auto"&gt;
        &lt;input
          value={input}
          onChange={e =&gt; setInput(e.target.value)}
          type="text"
          className="w-full px-5 py-2 bg-transparent border-2 outline-none border-zinc-600 rounded-xl placeholder:text-zinc-500 focus:border-white"
          placeholder="start typing ..."
        /&gt;
        &lt;button
          type="submit"
          className="px-5 py-2 text-sm font-normal text-blue-300 bg-blue-900 border-2 border-blue-900 active:scale-95 rounded-xl"
        &gt;
          Submit
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  )
}
</code></pre>
<p><code>handleSubmission</code>检查<code>input</code>（用户输入的待办事项）在使用<code>input.trim() !== ''</code>去除任何前导或尾随空格后是否不是空字符串。</p>
<p>如果它不为空，则使用<code>setTodos([...todos, input])</code>将<code>input</code>值添加到现有的<code>todos</code>数组中。这将创建一个新数组，其中包含所有之前的待办事项和在末尾添加的新待办事项。它使用<code>setInput('')</code>将<code>input</code>值重置为空字符串，这样输入字段就变为空的，准备好输入下一个待办事项。</p>
<p>现在，虽然你已经成功实现了创建待办事项的功能，但它还不能在屏幕上显示。</p>
<p>这是因为<code>&lt;AddTodo /&gt;</code>组件负责添加待办事项，而不是显示它们。</p>
<p>另一方面，<code>&lt;TodoList /&gt;</code>组件负责显示所有项目。为了弥合这一差距并在这些组件之间共享待办事项，你可以利用React Context的力量。</p>
<h2 id="what-is-react-context">什么是React上下文</h2>
<p>React Context API是React中的一个特性，它允许数据在不通过props显式传递的情况下被组件共享和访问。它提供了一种创建全局状态的方法，该状态可以被应用中的任何组件访问。</p>
<p>假设你有一个类似树的组件结构，其中某些数据需要被不同层级的多个组件访问。与其通过多层组件传递数据，你可以使用React Context为该数据创建一个中心存储。</p>
<p>它是这样工作的：</p>
<ol>
<li><strong>创建Context：</strong> 首先，你使用<code>createContext()</code>函数定义一个context。这将创建一个包含共享数据的context对象。</li>
<li><strong>提供Context：</strong> 你用<code>&lt;Context.Provider&gt;</code>包裹父组件或应用的特定部分。这个提供者组件接受一个<code>value</code>属性，你可以在其中传递你想要共享的数据。</li>
<li><strong>使用Context：</strong> 要在一个组件内访问共享数据，你使用React提供的<code>useContext()</code>钩子。通过将创建的context作为参数传递给<code>useContext()</code>，你可以访问共享数据，并在该组件内使用它。</li>
<li><strong>更新Context：</strong> 如果你需要更新共享数据，可以通过修改提供者组件中的值来实现。这个更改将自动传播到所有使用context的组件。</li>
</ol>
<p>React Context API简化了跨组件共享数据的过程，消除了手动传递prop的需要。</p>
<p>在你的情况下，你需要创建一个Context来在多个组件之间共享待办事项。让我们创建一个Context来看看这个机制在实践中是如何工作的。</p>
<h3 id="how-to-use-react-context-with-typescript">如何在TypeScript中使用React上下文</h3>
<p>在这一部分中，你将学习如何创建一个React Context来隔离应用逻辑，并提高你的应用的状态管理能力。</p>
<p>如果你打开<code>context/TodoContext.tsx</code>，你会看到以下代码：</p>
<pre><code class="language-tsx">// 📂./src/context/TodoContext.tsx

import React, { createContext } from 'react'
import { nanoid } from 'nanoid'
import { useLocalStorage } from 'usehooks-ts'

export const TodoContext = createContext&lt;undefined&gt;(undefined)

export const TodoProvider = (props: { children: React.ReactNode }) =&gt; {
  return (
    &lt;TodoContext.Provider value={undefined}&gt;
      {props.children}
    &lt;/TodoContext.Provider&gt;
  )
}

</code></pre>
<p>让我们逐步分解：</p>
<ul>
<li><code>TodoContext</code>是使用React提供的<code>createContext</code>函数创建的。它以未定义的值进行初始化。</li>
<li>此外，定义了一个名为<code>TodoProvider</code>的组件。它接受一个<code>children</code>属性，代表将被这个提供者包裹的子组件。</li>
<li>在<code>TodoProvider</code>组件内部，渲染了一个<code>&lt;TodoContext.Provider&gt;</code>组件。它包裹了<code>props.children</code>，允许子组件访问TodoContext。</li>
<li>目前为止，提供给<code>&lt;TodoContext.Provider&gt;</code>组件的值被设置为<code>undefined</code>。</li>
</ul>
<p>在接下来的部分中，你将通过学习TypeScript中所谓的<strong>接口</strong>来创建一个更复杂的Context。</p>
<h3 id="what-are-interfaces-in-typescript">TypeScript中的接口是什么</h3>
<p>在TypeScript中，接口是一种定义对象结构和形状的方式。它们允许你指定一个对象应该具有的属性及其类型。可以将接口视为一个蓝图或契约，描述一个对象应该具备的外观。</p>
<p>想象一下你正在建造一座房子。在开始施工之前，你会有一个蓝图，勾画出房子的设计和布局。类似地，TypeScript中的接口就像是一个对象的蓝图。</p>
<p>让我们来看一个简单的接口示例：</p>
<pre><code class="language-ts">interface Person {
  name: string;
  age: number;
}
</code></pre>
<p>在这个示例中，我们定义了一个名为<code>Person</code>的接口，描述了一个人对象的结构。它指定一个人对象应该有两个属性：<code>name</code>，其类型应为<code>string</code>，和<code>age</code>，其类型应为<code>number</code>。</p>
<p>让我们考虑你的Todo Context以及你想传递给其消费者的属性。在这种情况下，你需要一个定义所需属性的接口，包括包含所有待办事项的字符串数组，以及一个接受字符串并将其添加到待办事项列表中的函数。</p>
<pre><code class="language-tsx">interface TodoContextProps {
  todos: string[]
  addTodo: (text: string) =&gt; void
}
</code></pre>
<p><code>TodoContextProps</code>接口指定了TodoContext中期望的属性结构。它有两个属性：</p>
<ol>
<li><code>todos</code>：表示待办事项的字符串数组。这个属性包含了所有现有的待办事项。</li>
<li><code>addTodo</code>：一个接受类型为字符串（<code>text</code>）的参数并返回<code>void</code>类型的函数。这个函数负责将新的待办事项添加到列表中。它接受新的待办事项作为输入，并执行必要的操作，但不返回任何值。</li>
</ol>
<h3 id="how-to-use-typescript-interfaces-with-react-context">如何在React Context中使用TypeScript接口</h3>
<p>现在你已经了解了TypeScript接口的好处，是时候通过整合这个接口来增强你的Context了：</p>
<pre><code class="language-tsx">// 📂./src/context/TodoContext.tsx

import React, { createContext, useState } from 'react'
import { nanoid } from 'nanoid'
import { useLocalStorage } from 'usehooks-ts'

interface TodoContextProps {
  todos: string[]
  addTodo: (text: string) =&gt; void
}
export const TodoContext = createContext&lt;TodoContextProps | undefined&gt;(
  undefined,
)

export const TodoProvider = (props: { children: React.ReactNode }) =&gt; {
  const [todos, setTodos] = useState&lt;string[]&gt;([])

  // ::: ADD NEW TODO :::
  const addTodo = (text: string) =&gt; {
    setTodos([...todos, text])
  }

  const value: TodoContextProps = {
    todos,
    addTodo,
  }

  return (
    &lt;TodoContext.Provider value={value}&gt;{props.children}&lt;/TodoContext.Provider&gt;
  )
}
</code></pre>
<p>在这个更新的代码中，与之前的版本相比有显著的变化。这些变化引入了TypeScript，并修改了TodoContext和TodoProvider组件：</p>
<ol>
<li>这里，<code>TodoContextProps</code>指定它应该有两个属性：<code>todos</code>，表示待办事项的字符串数组，以及<code>addTodo</code>，一个接受字符串参数并返回void（无返回值）的函数。</li>
<li>现在使用<code>createContext</code>创建了<code>TodoContext</code>，并用<code>TodoContextProps | undefined</code>类型进行初始化。这意味着context值可以是<code>TodoContextProps</code>类型或未定义。</li>
<li><code>TodoProvider</code>组件现在使用<code>useState</code>钩子初始化<code>todos</code>状态。它使用一个字符串数组来跟踪待办事项。</li>
<li>引入了一个新函数<code>addTodo</code>，它接受一个字符串<code>text</code>作为参数。它使用<code>setTodos</code>函数通过将新的待办事项追加到现有数组来更新<code>todos</code>状态。</li>
<li>创建context的值：<code>value</code>变量被赋值为一个<code>TodoContextProps</code>类型的对象，包含<code>todos</code>数组和<code>addTodo</code>函数。</li>
<li>提供context值：<code>&lt;TodoContext.Provider&gt;</code>组件包裹<code>props.children</code>，并将value属性设置为<code>value</code>，它向子组件提供<code>todos</code>和<code>addTodo</code>。</li>
</ol>
<p>总而言之，你正在使用TypeScript为TodoContextProps定义一个接口，使用useState和自定义函数添加新的待办事项，并向子组件提供更新后的context值。</p>
<h3 id="how-to-create-a-custom-hook-to-consume-react-context">如何创建一个自定义钩子来使用React Context</h3>
<p>为了使用context提供的值，你需要创建一个自定义钩子来使用这个context，并将其值提供给子组件。打开<code>context/useTodo.ts</code>并添加以下代码：</p>
<pre><code class="language-tsx">// 📂./src/context/useTodo.ts

import { useContext } from 'react'
import { TodoContext } from './TodoContext'

export const useTodo = () =&gt; {
  const context = useContext(TodoContext)

  if (!context) {
    throw new Error('useTodo must be used within a TodoProvider')
  }

  return context
}
</code></pre>
<p>让我们逐步分解：</p>
<ol>
<li>你从'react'模块导入<code>useContext</code>钩子，并从<code>./TodoContext</code>文件导入<code>TodoContext</code>。</li>
<li>在钩子内部，调用<code>useContext</code>钩子并以<code>TodoContext</code>作为参数。这样连接到<code>TodoContext</code>并检索其当前值。</li>
<li>如果<code>context</code>值是<code>undefined</code>，这意味着<code>useTodo</code>钩子正在<code>TodoProvider</code>的范围之外使用。在这种情况下，会抛出一个错误消息，内容为'<code>useTodo</code>必须在<code>TodoProvider</code>内部使用'。</li>
</ol>
<p>总体来说，这段代码允许你创建一个名为<code>useTodo</code>的自定义钩子，可以在你的组件中使用。</p>
<p>通过调用这个钩子，你可以访问<code>TodoContext</code>并检索其值，其中包括在<code>TodoProvider</code>中定义的与待办事项相关的数据和函数。</p>
<p>它还确保<code>useTodo</code>钩子只在<code>TodoProvider</code>的范围内使用，以维护正确的使用方式并防止任何错误。</p>
<p>接下来，你需要用TodoProvider组件包裹整个应用程序。这确保了通过使用<code>useTodo</code>钩子，context值可以被其子组件访问：</p>
<pre><code class="language-tsx">// 📂 ./src/main.tsx

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  &lt;React.StrictMode&gt;
    &lt;TodoProvider&gt;
      &lt;App /&gt;
    &lt;/TodoProvider&gt;
  &lt;/React.StrictMode&gt;,
)
</code></pre>
<p><code>&lt;TodoProvider&gt;</code>包裹了整个应用程序，并提供了管理待办事项相关数据所需的context。</p>
<p>现在，让我们在<code>&lt;AddTodo /&gt;</code>组件中集成<code>useTodo</code>钩子，以通过context高效管理待办事项。此外，让我们实现toast通知，以根据用户交互提供反馈：</p>
<pre><code class="language-tsx">//📂./src/components/AddTodo.tsx

import React, { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { useTodo } from '../context/useTodo'
import { Input } from './Input'

export const AddTodo = () =&gt; {
  const [input, setInput] = useState&lt;string&gt;('')
  const inputRef = useRef&lt;HTMLInputElement&gt;(null)
  const { addTodo } = useTodo()

  useEffect(() =&gt; {
    if (inputRef.current) {
      inputRef.current.focus()
    }
  }, [])

  const handleSubmission = (e: React.FormEvent) =&gt; {
    e.preventDefault()
    if (input.trim() !== '') {
      addTodo(input)
      setInput('')
      toast.success('Todo added successfully!')
    } else {
      toast.error('Todo field cannot be empty!')
    }
  }

  return (
    &lt;form onSubmit={handleSubmission}&gt;
      &lt;div className="flex items-center w-full max-w-lg gap-2 p-5 m-auto"&gt;
        &lt;Input
          ref={inputRef}
          type="text"
          placeholder="start typing ..."
          value={input}
          onChange={e =&gt; setInput(e.target.value)}
        /&gt;
        &lt;button
          type="submit"
          className="px-5 py-2 text-sm font-normal text-blue-300 bg-blue-900 border-2 border-blue-900 active:scale-95 rounded-xl"
        &gt;
          Submit
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  )
}
</code></pre>
<ol>
<li>行<code>const { addTodo } = useTodo()</code>使用<code>useTodo</code>钩子从待办事项context中检索<code>addTodo</code>函数。这使我们能够添加新的待办事项。</li>
<li>行<code>toast.success('Todo added successfully!')</code>显示一个成功的toast通知，指示待办事项已成功添加。</li>
<li>行<code>toast.error('Todo field cannot be empty!')</code>在尝试提交时如果待办事项字段为空，则显示一个错误的toast通知。</li>
<li>如果<code>input</code>值（去除空格）不为空，则调用<code>addTodo</code>函数并传入输入值，清除<code>input</code>状态，并显示成功的toast通知。</li>
<li>如果<code>input</code>值为空，则显示一个错误的toast通知，指出待办事项字段不能为空。</li>
</ol>
<p>这段代码集成了<code>useTodo</code>钩子，通过context管理待办事项。它捕获用户输入，添加待办事项，并显示toast通知，以提供关于添加待办事项成功或失败的反馈。</p>
<p>现在，让我们也修改<code>&lt;TodoList /&gt;</code>组件，并在屏幕上显示待办事项。打开<code>components/TodoList.tsx</code>并添加以下代码：</p>
<pre><code class="language-tsx">//📂./src/components/TodoList.tsx

import { useTodo } from '../context/useTodo'
import { SiStarship } from 'react-icons/si'

export const TodoList = () =&gt; {
  const { todos } = useTodo()

  if (!todos.length) {
    return (
      &lt;div className="max-w-lg px-5 m-auto"&gt;
        &lt;h1 className="flex flex-col items-center gap-5 px-5 py-10 text-xl font-bold text-center rounded-xl bg-zinc-900"&gt;
          &lt;SiStarship className="text-5xl" /&gt;
          You have nothing to do!
        &lt;/h1&gt;
      &lt;/div&gt;
    )
  }

  return (
    &lt;ul className="grid max-w-lg gap-2 px-5 m-auto"&gt;
      {todos.map(todo =&gt; (
        &lt;li key={todo}&gt;{todo}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  )
}
</code></pre>
<ol>
<li>导入语句<code>import { useTodo } from '../context/useTodo'</code>从自定义context中导入<code>useTodo</code>钩子，使我们能够访问<code>todos</code>数组。</li>
<li>如果<code>todos</code>数组为空（<code>!todos.length</code>），意味着没有待办事项，将显示一条消息表明没有要做的事情。</li>
<li>如果<code>todos</code>数组中有待办事项，则渲染一个无序列表（<code>&lt;ul&gt;</code>）。</li>
<li>在<code>&lt;ul&gt;</code>内部，使用<code>map</code>函数遍历<code>todos</code>数组。对于每个待办事项，创建一个带有唯一<code>key</code>的列表项（<code>&lt;li&gt;</code>），<code>key</code>设置为待办事项的值。</li>
<li>然后将待办事项本身显示在列表项中。</li>
</ol>
<p>这个组件使用<code>useTodo</code>钩子从context中检索<code>todos</code>数组。如果没有待办事项，它会显示一条消息。如果有待办事项，它会渲染一个无序列表，并为每个待办事项填充列表项。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/ezgif-5-ff3ed7ffc5.gif" alt="添加待办事项和显示toast通知" width="600" height="400" loading="lazy"></p>
<p>上图显示添加待办事项和显示toast通知。</p>
<p>到目前为止做得很好！你现在有了一个基本的待办事项应用程序。是时候增加一些令人兴奋的功能，进一步提升你的应用了。</p>
<h2 id="how-to-define-an-interface-for-todo-items">如何为待办事项定义一个接口</h2>
<p>在这一部分中，你将基于上一节中的现有context进行构建，并增强它，以创建具有额外功能的更复杂的待办事项。</p>
<p>每个待办事项由三个属性组成：</p>
<ul>
<li><strong>id：</strong> 一个独特的字符串，作为该项的标识符</li>
<li><strong>text：</strong> 一个简单的字符串，代表待办事项的内容</li>
<li><strong>status：</strong> 待办事项的状态，可以是“未完成”或“已完成”</li>
</ul>
<p>基于上述信息，适当的待办事项接口如下所示：</p>
<pre><code class="language-ts">interface Todo {
  id: string
  text: string
  status: 'undone' | 'completed'
}
</code></pre>
<p>为了将Todo接口集成到你的context中，我们将进行必要的更新和修改，以有效地利用这个增强的context：</p>
<pre><code class="language-tsx">//📂./src/context/TodoContext.tsx

import React, { createContext, useState } from 'react'
import { nanoid } from 'nanoid'
import { useLocalStorage } from 'usehooks-ts'

interface TodoContextProps {
  todos: Todo[]
  addTodo: (text: string) =&gt; void
}

export interface Todo {
  id: string
  text: string
  status: 'undone' | 'completed'
}

export const TodoContext = createContext&lt;TodoContextProps | undefined&gt;(
  undefined,
)

export const TodoProvider = (props: { children: React.ReactNode }) =&gt; {
  const [todos, setTodos] = useState&lt;Todo[]&gt;([])

  // ::: ADD NEW TODO :::
  const addTodo = (text: string) =&gt; {
    const newTodo: Todo = {
      id: nanoid(),
      text,
      status: 'undone',
    }

    setTodos([...todos, newTodo])
  }

  const value: TodoContextProps = {
    todos,
    addTodo,
  }

  return (
    &lt;TodoContext.Provider value={value}&gt;{props.children}&lt;/TodoContext.Provider&gt;
  )
}
</code></pre>
<p>以下是context中变更的解释：</p>
<p><strong>Todo接口：</strong></p>
<ul>
<li>Todo接口定义了待办事项的结构。</li>
<li>它包括三个属性：id（一个字符串），text（一个代表待办事项内容的字符串），以及status（一个可以取值为'undone'或'completed'的字符串）。</li>
<li>这个接口有助于确保待办事项具有一致的属性和数据类型。</li>
</ul>
<p><strong>useState&lt;Todo[]&gt;：</strong></p>
<ul>
<li>useState钩子用于在函数组件中管理状态。</li>
<li>在这种情况下，<code>useState&lt;Todo[]&gt;</code>初始化了一个名为"todos"的状态变量作为Todo项目的数组。</li>
<li>"todos"状态变量将用于存储和更新待办事项。</li>
</ul>
<p><strong><code>addTodo</code>函数和<code>newTodo</code>变量：</strong></p>
<ul>
<li>addTodo函数是一个回调函数，它接受一个文本参数（字符串）。</li>
<li>在addTodo函数内部，声明了一个名为newTodo的变量作为Todo对象。</li>
<li>newTodo对象使用nanoid()函数生成的唯一id、提供的文本以及初始状态'undone'创建。</li>
<li>调用useState中的setTodos函数来更新todos状态，通过将newTodo对象添加到现有的todos数组中。</li>
<li>这允许向列表中添加新的待办事项。</li>
</ul>
<p>现在，你需要更新<code>&lt;TodoList /&gt;</code>组件以反映你对context所做的更改：</p>
<pre><code class="language-tsx">//📂./src/components/TodoList.tsx

import { useTodo } from '../context/useTodo'
import { SiStarship } from 'react-icons/si'

export const TodoList = () =&gt; {
  const { todos } = useTodo()

  if (!todos.length) {
    return (
      &lt;div className="max-w-lg px-5 m-auto"&gt;
        &lt;h1 className="flex flex-col items-center gap-5 px-5 py-10 text-xl font-bold text-center rounded-xl bg-zinc-900"&gt;
          &lt;SiStarship className="text-5xl" /&gt;
          You have nothing to do!
        &lt;/h1&gt;
      &lt;/div&gt;
    )
  }

  return (
    &lt;ul className="grid max-w-lg gap-2 px-5 m-auto"&gt;
      {todos.map(todo =&gt; (
        &lt;li key={todo.id}&gt;{todo.text}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  )
}
</code></pre>
<p>通过这个更新的代码，现在每个渲染的待办事项的id被用作每个待办事项的key属性，待办事项的text被用来显示每个待办事项的内容。</p>
<p>现在，让我们创建一个自定义的React组件来适当地显示每个待办事项，并在我们的应用中引入诸如编辑、删除和更新单个待办事项等额外功能。</p>
<h2 id="how-to-build-a-custom-react-component-for-displaying-todo-items">如何构建一个自定义的React组件来显示待办事项</h2>
<p>在这一部分中，你将创建一个自定义的React组件，用于处理每个单独待办事项的显示和管理。</p>
<p>打开<code>components/TodoItem.tsx</code>并添加以下代码：</p>
<pre><code class="language-tsx">//📂./src/components/TodoItem.tsx

export const TodoItem = (props: { todo: Todo }) =&gt; {
  const { todo } = props

  return (
    &lt;motion.li
      layout
      className={cn(
        'p-5 rounded-xl bg-zinc-900',
        todo.status === 'completed' &amp;&amp; 'bg-opacity-50 text-zinc-500',
      )}
    &gt;
      &lt;motion.span
        layout
        style={{
          textDecoration: todo.status === 'completed' ? 'line-through' : 'none',
        }}
      &gt;
        {todo.text}
      &lt;/motion.span&gt;
    &lt;/motion.li&gt;
  )
}
</code></pre>
<p><code>&lt;TodoItem /&gt;</code>负责渲染单个待办事项：</p>
<ul>
<li>该组件接受一个名为<code>props</code>的属性，这是一个包含名为<code>todo</code>的属性的对象。<code>todo</code>属性是<code>Todo</code>类型，代表单个待办事项。</li>
<li>在组件内部，使用解构赋值从<code>props</code>对象中提取<code>todo</code>属性。</li>
<li>使用Framer Motion的<code>motion.li</code>组件提供动画效果。它代表一个列表项（<code>&lt;li&gt;</code>），并支持布局动画。</li>
<li><code>className</code>属性使用<code>cn</code>实用函数（来自<code>classnames</code>库）根据<code>todo.status</code>条件性地应用CSS类。如果待办事项已完成，它会添加半透明背景和文本颜色的类。</li>
<li>在列表项内部，使用<code>motion.span</code>组件包裹待办事项文本。它同样支持布局动画。</li>
<li>span元素的样式根据<code>todo.status</code>设置。如果待办事项已完成，会应用删除线文本装饰。</li>
<li><code>{todo.text}</code>表达式渲染待办事项的文本内容。</li>
</ul>
<p>TodoItem接收一个待办事项作为属性，并根据待办事项的状态，使用可选动画、样式和条件CSS类进行渲染。</p>
<p>现在让我们修改<code>&lt;TodoList /&gt;</code>组件，以使用<code>&lt;TodoItem /&gt;</code>组件：</p>
<pre><code class="language-tsx">//📂./src/components/TodoList.tsx

import { TodoItem } from './TodoItem'
import { useTodo } from '../context/useTodo'
import { SiStarship } from 'react-icons/si'
import { motion } from 'framer-motion'

export const TodoList = () =&gt; {
  const { todos } = useTodo()

  if (!todos.length) {
    return (
      &lt;div className="max-w-lg px-5 m-auto"&gt;
        &lt;h1 className="flex flex-col items-center gap-5 px-5 py-10 text-xl font-bold text-center rounded-xl bg-zinc-900"&gt;
          &lt;SiStarship className="text-5xl" /&gt;
          You have nothing to do!
        &lt;/h1&gt;
      &lt;/div&gt;
    )
  }

  return (
    &lt;motion.ul className="grid max-w-lg gap-2 px-5 m-auto"&gt;
      {todos.map(todo =&gt; (
        &lt;TodoItem todo={todo} key={todo.id} /&gt;
      ))}
    &lt;/motion.ul&gt;
  )
}
</code></pre>
<p>以下是<code>&lt;TodoList /&gt;</code>中所做更改的解释：</p>
<p><strong>导入额外的依赖：</strong></p>
<ul>
<li>现在的代码从<code>framer-motion</code>库中导入了<code>motion</code>组件。这允许在组件中实现动画效果。</li>
</ul>
<p><strong>渲染TodoItem组件：</strong></p>
<ul>
<li>之前，待办事项被作为简单的列表项（<code>&lt;li&gt;</code>）直接在TodoList组件中渲染。</li>
<li>在更新的版本中，导入（<code>import { TodoItem } from './TodoItem'</code>）并使用TodoItem组件来渲染每个待办事项。</li>
<li>TodoItem组件传递了一个代表单个待办事项的<code>todo</code>属性。</li>
<li>同时为每个TodoItem组件提供了<code>key</code>属性，确保每个渲染的待办事项具有唯一标识符。</li>
</ul>
<p><strong>使用motion组件包裹列表：</strong></p>
<ul>
<li><code>&lt;ul&gt;</code>元素现在被<code>&lt;motion.ul&gt;</code>组件包裹，以使用<code>framer-motion</code>库启用动画效果。</li>
<li>这允许在添加、移除或更新待办事项时实现动态和平滑的过渡。</li>
</ul>
<p>总的来说，更新后的TodoList组件使用<code>framer-motion</code>的<code>motion</code>组件引入了动画，并用<code>&lt;TodoItem /&gt;</code>组件替换了直接渲染待办事项的方式。</p>
<p>现在你已经成功创建了<code>&lt;TodoItem /&gt;</code>组件，让我们将重点转向实现必要的功能，以启用使用Todo Context和TodoItem组件来编辑、删除和更新每个待办事项。</p>
<h2 id="how-to-implement-functionality-edit-delete-and-update-todo-items">如何实现功能：编辑、删除和更新待办事项</h2>
<p>在这一部分中，你将通过增加额外功能来增强你的待办事项应用。</p>
<p>首先，你将在待办事项context中实现处理这些功能所需的逻辑。然后，你将向<code>&lt;TodoItem /&gt;</code>组件添加相应的JSX，以引入交互性，并使用户能够与应用互动。</p>
<p>正如你所记得的，你使用context处理了向应用添加待办事项，你将采用类似的方法来处理编辑、删除和更新功能。</p>
<p>这些操作的逻辑将被封装在待办事项context中，将使用useTodo钩子在<code>&lt;TodoItem /&gt;</code>组件中利用这些逻辑。你还将把待办事项存储在浏览器的本地存储中，以确保用户离开应用时不会丢失他们的进度。</p>
<p>打开<code>context/TodoContext.tsx</code>并添加以下代码：</p>
<pre><code class="language-tsx">// 📂./src/context/TodoContext.tsx

import React, { createContext } from 'react'
import { nanoid } from 'nanoid'
import { useLocalStorage } from 'usehooks-ts'

interface TodoContextProps {
  todos: Todo[]
  addTodo: (text: string) =&gt; void
  deleteTodo: (id: string) =&gt; void
  editTodo: (id: string, text: string) =&gt; void
  updateTodoStatus: (id: string) =&gt; void
}

export interface Todo {
  id: string
  text: string
  status: 'undone' | 'completed'
}

export const TodoContext = createContext&lt;TodoContextProps | undefined&gt;(
  undefined,
)

export const TodoProvider = (props: { children: React.ReactNode }) =&gt; {
  const [todos, setTodos] = useLocalStorage&lt;Todo[]&gt;('todos', [])

  // ::: ADD NEW TODO :::
  const addTodo = (text: string) =&gt; {
    const newTodo: Todo = {
      id: nanoid(),
      text,
      status: 'undone',
    }

    setTodos([...todos, newTodo])
  }

  // ::: DELETE A TODO :::
  const deleteTodo = (id: string) =&gt; {
    setTodos(prevTodos =&gt; prevTodos.filter(todo =&gt; todo.id !== id))
  }

  // ::: EDIT A TODO :::
  const editTodo = (id: string, text: string) =&gt; {
    setTodos(prevTodos =&gt; {
      return prevTodos.map(todo =&gt; {
        if (todo.id === id) {
          return { ...todo, text }
        }
        return todo
      })
    })
  }

  // ::: UPDATE TODO STATUS :::
  const updateTodoStatus = (id: string) =&gt; {
    setTodos(prevTodos =&gt; {
      return prevTodos.map(todo =&gt; {
        if (todo.id === id) {
          return {
            ...todo,
            status: todo.status === 'undone' ? 'completed' : 'undone',
          }
        }
        return todo
      })
    })
  }

  const value: TodoContextProps = {
    todos,
    addTodo,
    deleteTodo,
    editTodo,
    updateTodoStatus,
  }

  return (
    &lt;TodoContext.Provider value={value}&gt;{props.children}&lt;/TodoContext.Provider&gt;
  )
}
</code></pre>
<p>以下是正在发生的事情的解释：</p>
<p><strong>定义TodoContextProps：</strong></p>
<ul>
<li>TodoContextProps是一个接口，指定了TodoContext的值的结构。</li>
<li>它包括诸如todos（一个Todo项的数组）之类的属性，以及添加、删除、编辑和更新待办事项状态的函数。</li>
</ul>
<p><strong>实现<code>addTodo</code>：</strong></p>
<ul>
<li>addTodo函数接受一个文本参数，使用nanoid生成一个唯一ID，并用提供的文本和初始状态'undone'创建一个新的待办事项对象。</li>
<li>它使用useLocalStorage提供的setTodos函数，通过将newTodo追加到现有的todos数组来更新todos状态。</li>
</ul>
<p><strong>实现<code>deleteTodo</code>：</strong></p>
<ul>
<li>deleteTodo函数接受一个id参数，并使用setTodos函数从todos状态中过滤掉具有匹配id的待办事项。</li>
</ul>
<p><strong>实现<code>editTodo</code>：</strong></p>
<ul>
<li>editTodo函数接受一个id和文本参数。</li>
<li>它使用setTodos函数遍历todos状态，并更新具有匹配id的待办事项的文本。</li>
</ul>
<p><strong>实现<code>updateTodoStatus</code>：</strong></p>
<ul>
<li>updateTodoStatus函数接受一个id参数。</li>
<li>它使用setTodos函数遍历todos状态，并在'undone'和'completed'之间切换具有匹配id的待办事项的状态。</li>
</ul>
<p><strong>提供值并渲染子组件：</strong></p>
<ul>
<li>使用todos数组和定义的函数创建了一个value对象。</li>
<li>它作为value属性传递给TodoContext.Provider组件，以向其嵌套的子组件提供定义的值。</li>
</ul>
<p>总而言之，<code>TodoContext</code>和<code>TodoProvider</code>处理与管理待办事项相关的状态和逻辑。它们通过TodoContext提供必要的函数和数据供子组件使用，如<code>&lt;TodoItem /&gt;</code>，以执行添加、删除、编辑和更新待办事项等操作。</p>
<p>现在，让我们加入相应的JSX，使用户能够与你刚刚实现的逻辑进行交互。打开<code>components/TodoItem.tsx</code>并添加以下代码：</p>
<pre><code class="language-tsx">//📂./src/components/TodoItem.tsx

import { useEffect, useRef, useState } from 'react'
import { Todo } from '../context/TodoContext'
import { useTodo } from '../context/useTodo'
import { Input } from './Input'
import { BsCheck2Square } from 'react-icons/bs'
import { TbRefresh } from 'react-icons/tb'
import { FaRegEdit } from 'react-icons/fa'
import { RiDeleteBin7Line } from 'react-icons/ri'
import { toast } from 'react-hot-toast'
import cn from 'classnames'
import { motion } from 'framer-motion'

export const TodoItem = (props: { todo: Todo }) =&gt; {
  const { todo } = props

  const [editingTodoText, setEditingTodoText] = useState&lt;string&gt;('')
  const [editingTodoId, setEditingTodoId] = useState&lt;string | null&gt;(null)

  const { deleteTodo, editTodo, updateTodoStatus } = useTodo()

  const editInputRef = useRef&lt;HTMLInputElement&gt;(null)

  useEffect(() =&gt; {
    if (editingTodoId !== null &amp;&amp; editInputRef.current) {
      editInputRef.current.focus()
    }
  }, [editingTodoId])

  const handleEdit = (todoId: string, todoText: string) =&gt; {
    setEditingTodoId(todoId)
    setEditingTodoText(todoText)

    if (editInputRef.current) {
      editInputRef.current.focus()
    }
  }

  const handleUpdate = (todoId: string) =&gt; {
    if (editingTodoText.trim() !== '') {
      editTodo(todoId, editingTodoText)
      setEditingTodoId(null)
      setEditingTodoText('')
      toast.success('Todo updated successfully!')
    } else {
      toast.error('Todo field cannot be empty!')
    }
  }

  const handleDelete = (todoId: string) =&gt; {
    deleteTodo(todoId)
    toast.success('Todo deleted successfully!')
  }

  const handleStatusUpdate = (todoId: string) =&gt; {
    updateTodoStatus(todoId)
    toast.success('Todo status updated successfully!')
  }

  return (
    &lt;motion.li
      layout
      key={todo.id}
      className={cn(
        'p-5 rounded-xl bg-zinc-900',
        todo.status === 'completed' &amp;&amp; 'bg-opacity-50 text-zinc-500',
      )}
    &gt;
      {editingTodoId === todo.id ? (
        &lt;motion.div layout className="flex gap-2"&gt;
          &lt;Input
            ref={editInputRef}
            type="text"
            value={editingTodoText}
            onChange={e =&gt; setEditingTodoText(e.target.value)}
          /&gt;
          &lt;button
            className="px-5 py-2 text-sm font-normal text-orange-300 bg-orange-900 border-2 border-orange-900 active:scale-95 rounded-xl"
            onClick={() =&gt; handleUpdate(todo.id)}
          &gt;
            Update
          &lt;/button&gt;
        &lt;/motion.div&gt;
      ) : (
        &lt;div className="flex flex-col gap-5"&gt;
          &lt;motion.span
            layout
            style={{
              textDecoration:
                todo.status === 'completed' ? 'line-through' : 'none',
            }}
          &gt;
            {todo.text}
          &lt;/motion.span&gt;
          &lt;div className="flex justify-between gap-5 text-white"&gt;
            &lt;button onClick={() =&gt; handleStatusUpdate(todo.id)}&gt;
              {todo.status === 'undone' ? (
                &lt;span className="flex items-center gap-1"&gt;
                  &lt;BsCheck2Square /&gt;
                  Mark Completed
                &lt;/span&gt;
              ) : (
                &lt;span className="flex items-center gap-1"&gt;
                  &lt;TbRefresh /&gt;
                  Mark Undone
                &lt;/span&gt;
              )}
            &lt;/button&gt;
            &lt;div className="flex items-center gap-2"&gt;
              &lt;button
                onClick={() =&gt; handleEdit(todo.id, todo.text)}
                className="flex items-center gap-1 "
              &gt;
                &lt;FaRegEdit /&gt;
                Edit
              &lt;/button&gt;
              &lt;button
                onClick={() =&gt; handleDelete(todo.id)}
                className="flex items-center gap-1 text-red-500"
              &gt;
                &lt;RiDeleteBin7Line /&gt;
                Delete
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      )}
    &lt;/motion.li&gt;
  )
}
</code></pre>
<p>让我们关注<code>handleEdit</code>、<code>handleUpdate</code>、<code>handleDelete</code>和<code>handleStatusUpdate</code>函数及其工作方式：</p>
<p><strong><code>handleEdit</code>函数：</strong></p>
<p>当用户点击“编辑”按钮时调用此函数。它接受<code>todoId</code>（待办事项的唯一标识符）和<code>todoText</code>（待办事项当前文本）作为参数。</p>
<p>它将<code>editingTodoId</code>状态设置为<code>todoId</code>，将<code>editingTodoText</code>状态设置为<code>todoText</code>。此外，如果<code>editInputRef</code>（输入字段的引用）存在，它将使用<code>focus</code>方法将焦点设置在输入字段上。</p>
<p><strong><code>handleUpdate</code>函数：</strong></p>
<p>当用户在编辑待办事项后点击“更新”按钮时调用此函数。它接受<code>todoId</code>作为参数。</p>
<p>它首先检查修剪后的<code>editingTodoText</code>是否不为空。如果不为空，它将调用<code>useTodo</code>钩子中的<code>editTodo</code>函数，传递<code>todoId</code>和<code>editingTodoText</code>作为参数。然后将<code>editingTodoId</code>和<code>editingTodoText</code>状态分别重置为null和空字符串。</p>
<p>最后，如果更新成功则显示成功的toast消息，如果待办事项字段为空则显示错误的toast消息。</p>
<p><strong><code>handleDelete</code>函数：</strong></p>
<p>当用户点击“删除”按钮时调用此函数。它接受<code>todoId</code>作为参数。它将调用<code>useTodo</code>钩子中的<code>deleteTodo</code>函数，传递<code>todoId</code>作为参数。然后显示一条成功的toast消息，指示待办事项已成功删除。</p>
<p><strong><code>handleStatusUpdate</code>函数：</strong></p>
<p>当用户点击“标记完成”或“标记未完成”按钮时调用此函数。它接受<code>todoId</code>作为参数。</p>
<p>它将调用<code>useTodo</code>钩子中的<code>updateTodoStatus</code>函数，传递<code>todoId</code>作为参数。然后显示一条成功的toast消息，指示待办事项的状态已成功更新。</p>
<p>这些函数处理与在TodoItem组件中编辑、更新、删除和更新待办事项状态相关的交互和操作。</p>
<p>JSX显示待办事项的文本，并提供编辑、删除和更新其状态的选项。待办事项的外观和行为由<code>todo</code>对象的值和组件的状态变量决定。</p>
<p>如果待办事项正在被编辑，则显示输入字段和“更新”按钮。否则，将显示待办事项的文本，并提供标记为完成或未完成、编辑和删除的按钮。</p>
<p><code>handleEdit</code>、<code>handleUpdate</code>、<code>handleDelete</code>和<code>handleStatusUpdate</code>函数用作这些按钮的事件处理程序，使用户能够与待办事项进行交互和修改。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/ezgif-1-f7b9438717.gif" alt="Final todo app, a user adds an item, then edit and delete the todo item in order to display the app's functionality" width="600" height="400" loading="lazy"></p>
<p>以上是最终结果。</p>
<p>恭喜！你已经成功创建了一个具有基本功能的漂亮的待办事项应用。</p>
<p>通过本文所获得的知识，你现在已经准备好根据你的特定需求和偏好进一步增强和定制应用程序。</p>
<h2 id="conclusion">结论</h2>
<p>在整篇文章中，我们介绍了使用TypeScript进行React开发的基础知识，并学习了如何创建一个功能齐全的待办事项应用。</p>
<p>我们探索了状态管理、context和钩子等概念，使你能够添加、编辑、删除和更新待办事项。</p>
<p>有了这些知识，你现在已经准备好将这些原则应用到你的未来项目中，并使用React构建类型安全的应用程序。继续探索和实验新功能，将你的应用提升到一个新的水平。</p>
<p>你可以在<a href="https://twitter.com/Yazdun">Twitter</a>上关注我，我会在那里分享更多关于Web开发的有用提示。编码愉快！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 Three.js 在 React.js 应用程序中实现一个 Blender 模型 ]]>
                </title>
                <description>
                    <![CDATA[ 在这个分步指南中，你将学习如何建立一个带有基本动画的 Blender 文件。之后，你还将学习如何使用 React Three Fiber 来将 Three.js 集成到 React 应用程序中。熟悉这些概念可以帮助你以后开发的 React.js 应用程序脱颖而出。 🔐 以下是我们将涵盖的内容：  * 制作一个包括动画、材质和导出过程的 Blender 模型。  * 使用 React Three Fiber 构建与 Three.js 集成的 React.js 应用程序。  * 将个人创建的 Blender 模型整合到 React.js 应用程序中。 📝 先决条件：  * 建议对 3D 软件 Blender 有基本了解。  * 需要基本熟悉 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/blender-three-js-react-js/</link>
                <guid isPermaLink="false">65d17ae6ce929203f3f07ed3</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp.org ]]>
                </dc:creator>
                <pubDate>Sun, 18 Feb 2024 04:25:34 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/02/pexels-chevanon-photography-1335971.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/blender-three-js-react-js/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Implement a Blender Model in a React.js Application using Three.js</a>
      </p><!--kg-card-begin: markdown--><p>在这个分步指南中，你将学习如何建立一个带有基本动画的 Blender 文件。之后，你还将学习如何使用 React Three Fiber 来将 Three.js 集成到 React 应用程序中。熟悉这些概念可以帮助你以后开发的 React.js 应用程序脱颖而出。</p>
<h2 id=""><strong><strong><strong>🔐</strong></strong></strong> 以下是我们将涵盖的内容：</h2>
<ul>
<li>制作一个包括动画、材质和导出过程的 Blender 模型。</li>
<li>使用 React Three Fiber 构建与 Three.js 集成的 React.js 应用程序。</li>
<li>将个人创建的 Blender 模型整合到 React.js 应用程序中。</li>
</ul>
<h2 id=""><strong><strong><strong><strong><strong><strong><strong>📝</strong></strong></strong></strong></strong></strong></strong> 先决条件：</h2>
<ul>
<li>建议对 3D 软件 Blender 有基本了解。</li>
<li>需要基本熟悉 React.js。</li>
<li>无需具备之前使用 Three.js 的经验。</li>
</ul>
<h2 id="">目录</h2>
<ol>
<li><a href="#what-are-three-js-and-blender">💭 Three.js 和 Blender 是什么？</a></li>
<li><a href="#how-to-set-up-react-js-with-three-js">🔧 如何使用 Three.js 设置 React.js</a></li>
<li><a href="#how-to-create-a-blender-model"><strong>🔨</strong> 如何创建 Blender 模型</a></li>
<li><a href="#texture-baking-for-procedural-materials"><strong>✏️</strong> 程序材质的纹理烘焙</a></li>
<li><a href="#how-to-implement-the-blender-model-into-the-react-js-application"><strong>✒️</strong> 如何在 React.js 应用程序中实现 Blender 模型</a></li>
<li><a href="#additional-information"><strong>📄</strong> 其他信息</a></li>
<li><a href="#wrap-up"><strong>📋</strong> 总结</a></li>
</ol>
<h2 id="what-are-three-js-and-blender">💭 Three.js 和 Blender 是什么？</h2>
<p>Three.js 是一个 JavaScript 的库，通过提供的API可以让你在 Web 浏览器中展示 3D 模型。</p>
<p>利用 Three.js 可以帮助您将互动性和独特的功能无缝集成到您的网站中。</p>
<p>Blender 是一款专为制作和完善 3D 模型而定制的强大软件。它的多功能性提供了无限的机会，满足广泛的创意愿景。</p>
<p>除了显示功能之外，Blender 还为您提供了一系列工具，包括相机、灯光，甚至后期制作增强功能。</p>
<p>当一起使用时，这些工具可以激发无限的创造力，使您能够将您的艺术创作无缝地转化为您即将推出的网站项目。</p>
<h2 id="how-to-set-up-react-js-with-three-js">🔧 如何使用 Three.js 设置 React.js</h2>
<p>首先，安装 React.js 应用程序：</p>
<p><code>npx create-react-app my-app</code></p>
<p>然后， 需要安装 Three.js 和 <a href="https://docs.pmnd.rs/react-three-fiber/getting-started/installation">React Three Fiber</a>. React Three Fiber 充当 Three.js 的 React 渲染器，利用 React 组件的强大功能来简化 React.js 环境中 Three.js 的集成：</p>
<p><code>npm install three @react-three/fiber</code></p>
<p>为了丰富 Three.js 体验，我们还将集成 <a href="https://www.npmjs.com/package/@react-three/drei">React Three Drei</a>，这个包为各种 Three.js 应用场景引入了各种辅助工具，包括多个相机控件，例如：</p>
<p><code>npm install @react-three/drei</code></p>
<h3 id="gltftools">glTF Tools 扩展</h3>
<p>我还建议安装  <strong>glTF Tools</strong> 扩展。尽管不是绝对必要的，但此扩展可以帮助您执行各种任务。</p>
<p>如果您使用 Visual Studio Code 作为集成开发环境（IDE），则可以通过扩展选项卡方便地添加扩展。同样，此扩展是可选的，但它可以显著简化以后的某些流程。我将在整个教程中使用它：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/08/React1.0.PNG" alt="Visual Studio Code 中的 gltf Tools 扩展" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Visual Studio Code 中的 gltf Tools 扩展</figcaption>
</figure>
<h3 id="reactjsthreejs">在 React.js 中完成 Three.js 的设置</h3>
<p>React.js 应用程序的 <code>package.json</code> 文件中的依赖项现在如下所示：</p>
<pre><code class="language-JavaScript">"dependencies": {
    "@react-three/drei": "^9.80.2",
    "@react-three/fiber": "^8.13.6",
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "three": "^0.155.0",
    "web-vitals": "^2.1.4"
  },
</code></pre>
<p>package.json 文件中的依赖项，包括 React Three Fiber 和 React Three Drei</p>
<p>这些依赖项足以在 React.js 环境中使用 Three.js 完成各种任务。当然，你还可以根据需要集成其他依赖库，来实现其他功能。</p>
<p>除此之外，我还在 <a href="https://github.com/Matthes-Baer/blender-threejs-reactjs-article-app">GitHub</a> 上提供了本教程中的代码。这将使您能够快速访问信息，而无需滚动浏览整篇文章。</p>
<h2 id="how-to-create-a-blender-model">🔨 如何创建 Blender 模型</h2>
<p>首先，我们的初始任务涉及创建一个 Blender 模型，然后将其集成到我们的 React.js 应用程序中。 在这一阶段，让我们考虑 <strong>Layout</strong> 选项卡中的一个场景，其中我们有三个对象：两个球体和一个平面。您可以在 Blender 中使用 <code>Shift + A</code> 快捷键添加此类对象。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/08/blenderFirstImage.PNG" alt="Layout 选项卡中包含两个球体和一个平面的 Blender 场景" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Layout 选项卡中包含两个球体和一个平面的 Blender 场景</figcaption>
</figure>
<p>该构图仅包括一个平面和两个球体，没有其他细节。当然，您可以根据自己的喜好进行更精细的场景和模型设计。</p>
<p>但是，为了说明将自定义 Blender 模型整合到 React.js 的基本过程，这个基本示例将对我们足够了。</p>
<h3 id="">如何给模型添加动画</h3>
<p>现在，将我们的重点转移到向该 Blender 场景中的三个对象引入基本动画。这些动画可以促进对象的移动、旋转甚至缩放，从而实现动态变换。</p>
<p>为了在 Blender 中为对象添加动画，您可以切换到 <strong>Shading</strong> 和 <strong>Rendering</strong> 选项卡旁边的 <strong>Animation</strong> 选项卡。</p>
<p>在 “Animation” 选项卡中，你可以向特定帧添加点。例如，如果要将球体向左移动一点，请首先添加起始关键帧（右键单击对象，选择 “Insert Keyframe” ，然后选择 “Location” ）。</p>
<p>然后，在对象的动画时间轴上向前移动几帧，调整对象的位置，然后重复相同的过程。这样，您将拥有两个关键帧：初始关键帧和处于新位置的关键帧。</p>
<p>请记住，这一动作是朝一个方向的。如果想重复动画，它将移动到新位置，然后再跳转返回到其初始位置。</p>
<p>为了使运动更加平滑，可以复制初始关键帧并将其插入到末尾。这将使物体在到达新位置后再平滑的运动向后移动。这也是我在 Blender 模型中设置关键帧的方法。</p>
<p>当然，你可以添加更多关键帧来制作更复杂的动画。这只是开始使用 Blender 动画的基本介绍。与 Blender 其他方面一样，还有很多东西需要探索和学习。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/08/blenderSecondImage.PNG" alt="在 Animation 选项卡中的三个对象添加动画" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>在 Animation 选项卡中的三个对象添加动画</figcaption>
</figure>
<p>在这种情况下，没有必要彻底了解我们在此处添加的这些动画的细节。因此，实际上不需要知道第一个球体在动画中移动到哪个确切位置。</p>
<p>关键点是承认它们的存在，因为它们将在稍后阶段集成到我们的 React.js 应用程序中，以便我们可以在浏览器中激活它们。</p>
<h3 id="">如何添加颜色</h3>
<p>接下来，我们将为小球体和底层平面添加一些简单的颜色，例如，可以在 <strong>Shading</strong> 选项卡中执行此操作。</p>
<p>对于基本颜色，可以转到对象的 <strong>Material Properties</strong> 部分（右键单击对象，然后选择底部的倒数第二个类别）。但我想重点讨论您稍后可能会在模型中遇到的特定情况。因此，在本教程中，我将专门使用 <strong>Shading</strong> 选项卡来设置对象颜色。</p>
<p>在 <strong>Shading</strong> 选项卡中，可以在屏幕底部添加节点。这些节点可以修改对象的颜色和纹理等。你还会发现 <code>Vector</code> 和 <code>Shader</code> 节点，将它们组合起来可以为您的对象创建独特的视觉效果。</p>
<p>所有这些调整都适用于特定材料。因此，如果希望不同的对象具有相同的视觉效果，只需对它们应用相同的材​​质即可。</p>
<p>当我们第一次打开 <strong>Shading</strong> 选项卡来查找对象的材质时，最初会生成 <code>Principled BSDF</code> 和 <code>Material Output</code> 节点。这两个节点都是用来设置基本情况。</p>
<p><code>Principled BSDF</code> 有很多可以使用的设置。在这个例子中，我们只将 <code>Base Color</code> 属性更改为蓝色。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/08/blender3.0.PNG" alt="blender3.0" width="600" height="400" loading="lazy"></p>
<p>我们只需在 “Principled BSDF” 节点中调整 “Base Color” 的球体的材质。</p>
<p>对于较大的球体，使用类似的材料应用。但是，与 <code>Principled BSDF</code> 节点相比，我们将使用 <code>Glossy BSDF</code> 节点，它是 <code>Shader</code> 类别中的一个节点。这将帮助我们认识到您在为 React.js 应用程序设计 Blender 模型时可能会遇到的问题 - 您稍后会看到。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/08/blender3.2-1.PNG" alt="使用 “Glossy BSDF” 节点向大的球体添加材质" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>使用 “Glossy BSDF” 节点向大的球体添加材质</figcaption>
</figure>
<p>完成此操作后，我们就可以导出 Blender 模型了。请注意，此版本已大大简化。可以根据偏好进行更详细的模型设计。整体工作流程仍然相似。</p>
<h3 id="">如何导出模型</h3>
<p>要导出模型，我们需要生成 <code>.glb/.gltf</code> 文件。这一点至关重要，因为 Three.js 需要特定的文件格式来实现兼容性，在本例中，<code>.glb</code> 或 <code>.gltf</code> 文件符合库的要求。</p>
<p>因此，使用对象、动画、颜色等创建完模型后，您可以执行以下操作：</p>
<ol>
<li>单击左上角的 <strong>File</strong> 选项卡。</li>
<li>从列出的选项中选择 <strong>Export</strong>。现在，可以看到多种导出格式。</li>
<li>正如之前提到的那样，如果计划在应用程序中将模型与 Three.js 一起使用，则需要选择 <code>glTF 2.0 (.glb/.gltf)</code> 选项。</li>
</ol>
<p>选择此选项后，将弹​​出一个新的对话框。通过此窗口，您可以选择要保存文件的路径。</p>
<p>在此窗口的右侧，有其他选项。例如，您可以决定要导出哪些特定对象。在大多数情况下，默认设置应该可以正常工作。请记住，如有必要，可以根据自己的偏好调整这些设置。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/08/blender3.1-1.png" alt="blender3.1-1" width="600" height="400" loading="lazy"></p>
<p>请记住导出的格式是 <code>glTF 2.0 (.glb/.gltf)</code>。</p>
<h3 id="">如何将导出的模型可视化</h3>
<p>接下来，我们切换到 Visual Studio Code 并导航到导出文件的文件夹。</p>
<p>在此目录中，可以找到一个 <code>.glb</code> 文件。参考之前的 <strong>glTF Tools</strong> 扩展设置，只需右键单击 <code>.glb</code> 文件即可找到位于底部的两个附加选项，分别叫做 <code>glTF：从 GLB 导入</code> 和 <code>glTF：验证 GLB 或 GLTF 文件</code>。</p>
<p>在这种情况下，我们会选择 <code>glTF：从 GLB 导入</code> 选项。此操作将在同一文件夹中生成一个 <code>.gltf</code> 文件，在我们的例子中为 <code>blenderFile.gltf</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/08/blender4.0.png" alt="blender4.0" width="600" height="400" loading="lazy"></p>
<p>在 Blender 中利用 <code>glTF Tools</code> 扩展应用将初始导出的 <code>.glb</code> 文件生成一个 <code>.gltf</code> 文件。</p>
<p>我们选择这种方法是为了增强对 <code>.gltf</code> 文件的可访问性，可以通过在 Visual Studio Code 的 <strong>glTF Tools</strong> 扩展中直接查看。这对于在实际实施之前检查您的文件非常有帮助。</p>
<p>如果我们访问新创建的 <code>.gltf</code> 文件，我们可以观察到一些基于 Blender 模型的信息。请务必注意，具体内容可能根据每个工程的情况而有所不同，因为它们是为了反映 Blender 工程中对象和场景的属性而定制的。</p>
<p>如果我们看一下右上角，有一个符号看起来像一个立方体，旁边有一个圆锥体。通过单击此符号，可以直接在 IDE 中预览 Blender 场景。此功能只能由 <code>.gltf</code> 文件访问，在本例中不适用于 <code>.glb</code> 文件。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/08/blender4.5.png" alt="新创建的 .gltf 文件，可以选择直接在 Visual Studio Code 中查看模型（在右上角，用红色圈出）" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>新创建的 .gltf 文件，可以选择直接在 Visual Studio Code 中查看模型（在右上角，用红色圈出）</figcaption>
</figure>
<p>值得注意的是，不一定要通过 <strong>glTF Tools</strong> 扩展来完成这个过程。另外，一些网站允许上传文件进行可视化。但我个人发现这种在集成开发环境中的方法特别方便。它将整个过程集中起来，使你能够在实际实施之前评估文件的完整性。</p>
<p>如果你发现任何错误，这个做法可以让你提前发现问题是基于导出的文件有问题还是 React.js 应用中存在实施的疏忽。因此，我非常推荐在从 Blender 导出后评估你的模型文件。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/08/blender5.0.PNG" alt="在 Visual Studio Code 中使用 glTF Tools 查看 Blender 模型" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>在 Visual Studio Code 中使用 glTF Tools 查看 Blender 模型</figcaption>
</figure>
<p>在 Visual Studio Code 中通过使用 <strong>glTF Tools</strong> 扩展查看我们的 Blender 模型，我们可以看到所有三个对象都被正确识别。 小球体和平面都以其预期的颜色显示。</p>
<p>但是由于大球体没有指定预期的颜色，只是以默认的白色显示。</p>
<p>这种差异引发了一个问题：是什么导致了这种异常现象？ 像这样的情况证明了在将模型集成到 React.js 应用程序之前预览模型是多么有用。</p>
<p>通过在此阶段检查您的模型，就可以确认问题源于 Blender 模型本身而不是实现过程，因为我们还没有进行任何实现。</p>
<p>事实证明，这种实施前评估非常方便，使你能够在 React.js 中的实施过程之前诊断和解决潜在的复杂情况。</p>
<h2 id="texture-baking-for-procedural-materials">✏️ 程序材质的纹理烘焙</h2>
<p>简而言之，Blender 提供了使用程序节点处理材质的灵活性。虽然这些节点在 Blender 中可以无缝运行，但它们与其他游戏引擎或软件框架（如 Three.js ）并不直接兼容。</p>
<p>要了解更多信息，请考虑观看以下视频。 在短短10分钟内演示了纹理烘焙的过程，有效解决了当前的问题。</p>
<p>程序材质纹理烘焙教程</p>
<p>就我个人而言，当面对这一挑战并且最初不确定其性质时，我发现了这个视频，它是帮助我们深入了解该主题的宝贵资源。</p>
<p>在实际的具体场景中，虽然我们可能不会遇到视频中看到的那么复杂的情况，但仍然有可能面临着使用与各种软件工具缺乏直接兼容性的节点。</p>
<p>接下来，我们将简要介绍视频中提到的步骤。 但是，如果你有兴趣深入研究此过程，我强烈建议观看该视频。</p>
<h3 id="">如何创建文件纹理节点</h3>
<p>首先，在包含 <code>Glossy BSDFF</code> 节点的材质的 “Shading” 选项卡中，我们将引入一个 <code>Image Texture</code> 节点并将其连接到新图像（通过单击 <code>New</code>）。</p>
<p>我们将保留设置的默认值，这意味着宽度和高度为 <code>1024px</code>。 使用较大的值将大大延长我们将面临的处理时间。 不过，值得注意的是，较大的纹理可以提供更多细节并改善整体外观。</p>
<p>在目前的情况下，我们的目标是加快流程。 但对于更重要的项目，视觉质量可能至关重要。 在这种情况下，可能需要选择更高的分辨率。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/08/blender6.0-1.PNG" alt="创建一个 Image Texture 节点并使用默认设置为其分配一个新图像" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>创建一个 Image Texture 节点并使用默认设置为其分配一个新图像</figcaption>
</figure>
<h3 id="uv">如何应用智能 UV 投射流程</h3>
<p>接下来，我们需要使用 <strong>UV Editing</strong> 选项卡中的 <code>Smart UV Projec</code> 选项。 本质上，此操作将特定对象的面展开到纹理上。</p>
<p>此过程使我们能够在返回 <strong>Shading</strong> 选项卡后立即指定应该对纹理的哪些部分进行着色和修改。 为了使这个过程有效，我们必须选择大球体的所有面。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/08/blender7.0.png" alt="在 UV Editin 选项卡中选择对象的所有面并在其上应用 Smart UV Project" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>在 UV Editin 选项卡中选择对象的所有面并在其上应用 Smart UV Project</figcaption>
</figure>
<p>一旦我们完成此步骤并使用 <code>Smart UV Project</code> 过程的默认设置，左侧的图像（之前具有网格）现在将显示应用此过程的球体的形状。 在这种情况下，纹理似乎捕获了球体的各个角度。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/08/blender8.0.PNG" alt="Smart UV Project 后的纹理" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Smart UV Project 后的纹理</figcaption>
</figure>
<p>根据具体对象，单击 <code>Smart UV Project</code> 按钮，然后可能需要微调显示的设置。 如果遇到特定对象的其他问题，我之前分享的视频可以为你提供这方面的额外指导。</p>
<p>一般来说，为了缓解问题，应该在创建阶段优化对象布局。 例如，避免在特定位置引入过多边缘可以防止剪裁等问题。</p>
<h3 id="">烘焙过程</h3>
<p>现在，返回到 <strong>Shading</strong> 选项卡，我们将在其中访问右侧的 “Render Properties”（由小屏幕或电视符号表示）。 如果尚未选择，请选择 “Cycles” 作为 “Render Engine”。 然后导航到 “Bake” 类别，该类别在“ Performance” 类别下方。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/08/blender9.0-1.PNG" alt="在 Render Properties 里面 Shading 选项卡中的 Bake 选项" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>在 Render Properties 里面 Shading 选项卡中的 Bake 选项</figcaption>
</figure>
<p>使用现有的默认设置，可以通过单击 “Bake” 按钮继续，并且确保选择 “Image Texture” 节点和大球体。</p>
<p>请记住，我将“太阳”光集成到了场景中，因为此烘焙过程考虑了场景的照明。 如果没有足够的照明，结果可能会显得过暗。</p>
<p>经过一段时间的处理（如果您为 “Image Texture” 节点的图像使用了更大的尺寸，可能会更耗时），烘焙过程将完成。 这会导致纹理从 “Image Texture” 应用到图像。 现在可以通过常规的“正常”图像纹理来访问它，而不是从 “Glossy BSDF” 的 “Shader” 节点获取纹理。</p>
<p>然后可以建立从 “Image Texture” 节点到 “Material Output” 节点的连接，从而成功实现材质。 之前的方法是将 “Principled BSDF” 节点连接到 “Material Output” 节点的 “Surface” 输入。在此阶段，两个方法方法相比较，没有显着差异。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/08/blender10.0.PNG" alt="blender10.0" width="600" height="400" loading="lazy"></p>
<p>具有“烘焙”纹理的 “Image Texture” 节点与 “Material Output” 节点相连，而不是与 “Glossy BSDF” 节点连接。</p>
<h3 id="">如何看到最终结果</h3>
<p>现在，我们可以再次导出文件，使用 <strong>glTF Tools</strong> 扩展在 IDE 中重复之前的相同过程，并查看扩展名为 “.gltf” 的文件。 检查结果后，你可能会注意到它与我们在 Blender 中使用 “Glossy BSDF” 节点的版本不完全匹配。 这种差异主要归因于 Blender 场景中的照明条件。</p>
<p>请记住，我说明的方法不是烘焙过程的典型用法，因为在这种情况下，也可以使用 “Principled BSDF” 节点选择类似的基色，并且会实现几乎相同的解决方案。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/08/blender11.0.PNG" alt="使用 glTF Tools 完成的视图，包括大球体的“烘焙”纹理" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>使用 glTF Tools 完成的视图，包括大球体的“烘焙”纹理</figcaption>
</figure>
<p>根据个人经验介绍一下烘焙过程。 在某些情况下，我遇到了这样的场景：材质在 Blender 中的显示效果与在 React.js 应用程序中使用 Three.js 实现它们时的效果不同。 这种情况促使我探索烘焙的概念，结果证明这是一个有用的解决方案。</p>
<p>总而言之，如果你在使用 Three.js 的 React.js 应用程序中遇到材料无法按预期显示的情况，那么考虑烘焙过程并研究该主题可以提供有价值的见解。 这对于 Blender 新手来说尤其有益。</p>
<h2 id="how-to-implement-the-blender-model-into-the-react-js-application">✒️ 如何在 React.js 应用程序中实现 Blender 模型</h2>
<p>想要生成 Blender 文件，可以使用一个非常有用的命令（来源： <a href="https://github.com/pmndrs/gltfjsx">https://github.com/pmndrs/gltfjsx</a> ）：</p>
<p><code>npx gltfjsx public/blenderFileName.glb</code></p>
<p>需要注意的是，在此步骤中，需要将 Blender 文件存储在 React.js 应用程序的 “public” 文件夹中。 还值得强调的是，需要 React Three Drei 才能使用这个命令。 所以在我们的例子中，我们可以直接使用这个命令，而不需要任何额外的准备。</p>
<p>执行这个命令后，可以看到以下文件：</p>
<pre><code class="language-JavaScript">/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@6.1.4 public/blenderStuff/blenderFile.glb
*/

import { useLayoutEffect, useRef } from "react";
import { useGLTF, useAnimations } from "@react-three/drei";

export function Model(props) {
  const group = useRef();
  const { nodes, materials, animations } = useGLTF(
    "./blenderStuff/blenderFile.glb"
  );
  const { actions } = useAnimations(animations, group);

  return (
    &lt;group ref={group} {...props} dispose={null}&gt;
      &lt;group name="Scene"&gt;
        &lt;mesh
          name="Cube"
          geometry={nodes.Cube.geometry}
          material={materials.Material}
          position={[-0.07, 0.16, -0.27]}
          scale={[1, 0.03, 1]}
        /&gt;
        &lt;mesh
          name="Sphere"
          geometry={nodes.Sphere.geometry}
          material={materials["Material.002"]}
          position={[-0.62, 0.43, -0.79]}
          rotation={[-0.01, 0.11, -0.02]}
          scale={0.09}
        /&gt;
        &lt;mesh
          name="Sphere001"
          geometry={nodes.Sphere001.geometry}
          material={materials["Material.001"]}
          position={[0.4, 0.55, 0.15]}
          scale={0.41}
        /&gt;
      &lt;/group&gt;
    &lt;/group&gt;
  );
}

useGLTF.preload("./blenderStuff/blenderFile.glb");
</code></pre>
<p>blenderFile.jsx， 包括使其工作的基本代码</p>
<p>大概看一下，就可以看到这个过程添加了很多元素，所以基本上不需要自己添加太多。</p>
<p>配置的一个重要方面是在 useGLTF 钩子中设置路径。在我的实例中，要使用的准确路径是 ./blenderStuff/blenderFile.glb（同样适用于 useGLTF.preload() ）。这是因为我在 public 目录下创建了一个名为 blenderStuff 的子文件夹。</p>
<h3 id="canvas">如何添加 Canvas 包装器和其他组件</h3>
<p>完成此配置后，我们现在就可以使用 “Model” 组件了。 但为了有效地将这个 “Model” 组件集成到想要的位置，需要在父组件中进行一些调整。</p>
<p>在这个例子中，我选择在主 App.js 文件中实现它。 我为 App 的 CSS 类分配了 100vh 的高度，以确保所需的显示。</p>
<pre><code class="language-JavaScript">import "./App.css";
import { Model } from "./BlenderFile";
import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";

function App() {
  return (
    &lt;div className="App"&gt;
      &lt;Canvas camera={{ fov: 64, position: [-2, 2, 0] }}&gt;
        &lt;ambientLight intensity={5} /&gt;
        &lt;OrbitControls enableZoom={true} /&gt;
        &lt;Model /&gt;
      &lt;/Canvas&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>App.js，包括 “Canvas” 包装器、 “Model” 和其他组件</p>
<p>一般来说，你需要一个组件来封装所有 Three.js 相关元素。 在 “Canvas” 组件中，可以配置各种设置。 在本次的具体实例中，我正在调整初始相机位置。</p>
<p>光线对于组件起着至关重要的作用。 在我们的例子中，我们使用了 “ambientLight” 来为整个场景添加灯光。 如果没有足够的照明，尽管存在对象颜色，您的场景可能会显得非常暗甚至全黑。 您还可以使用其他光源，比如 “spotLight” 组件。</p>
<p>“OrbitControls” 组件可从 Drei 库进行访问，通过在浏览器中启用模型内的滚动和旋转来增强交互性。 这行代码极大地改进了用户交互选项。</p>
<p>请记住，“Canvas” 组件可以容纳多个模型。 还可以有选择地将 “OrbitControls” 等组件应用到特定的 Blender 模型，从而定制它们的行为。</p>
<p>为此，需要为要集成到 “Canvas” 中的每个场景构建一个父组件。 在每个新的父组件中，合并 Blender 模型组件以及想要添加的任何补充帮助器组件。</p>
<p>例如，当不同的模型需要不同的照明或独特的相机位置时，这种方法特别有利。</p>
<h3 id="">如何实现动画</h3>
<p>到目前为止，我们已经建立了一个功能性的 Three.js Canvas 环境，展示了我们的 Blender 模型。但是重要的是要记住，我们还引入了尚未启用的基本动画。</p>
<p>为了解决这个问题，我们可以利用预先实现的 “useAnimations” 钩子。</p>
<pre><code class="language-JavaScript">  const { actions, names } = useAnimations(animations, group);

  useLayoutEffect(() =&gt; {
    names.forEach((animation) =&gt; {
      actions?.[animation]?.play();
    });
  }, [actions, names]);
</code></pre>
<p>BlenderFile.jsx 中有关如何在页面渲染时激活模型动画的部分</p>
<p>通过合并此实现，与此 Blender 模型关联的所有动画将在页面呈现时开始播放。 此行为还包括每个动画的无限循环。</p>
<h2 id="additional-information">📄 其他信息</h2>
<p>虽然本教程主要关注使用 Three.js 将 Blender 模型集成到 React.js 应用程序中，但 Three.js 中还有一个我们没有涉及的未开发的领域。</p>
<p>尽管我们在这个基本示例中没有使用它，但您可以将后处理引入到 React.js 中的 Three.js 模型中。<a href="https://www.npmjs.com/package/@react-three/postprocessing">React Three Postprocessing</a> 库在这方面是一个非常有价值的工具。 它可以让您通过复杂的效果（如绽放或噪音效果）提升您的 Three.js 场景，这可以为您的可视化添加更高级的维度。</p>
<p>此外，在开发未来 Three.js 项目时，请考虑探索 <a href="https://docs.pmnd.rs/react-three-fiber/tutorials/using-with-react-spring">React Spring</a> 库，该库与 React Three Fiber 集成在一起。React Spring 提供了在 Three.js 场景中整合自定义动画的机会，可以在 Blender 中直接集成的任何动画之上进行。</p>
<p>例如，你可以通过单击使场景中的特定对象变大或变小。与 Three.js 的其他方面一样，这个方面可能会增强交互性，并且可能值得花时间了解。</p>
<p>顺便说一句，如果发现框架的运行速度较低，请考虑在浏览器设置中切换硬件加速，以提高性能。</p>
<h2 id="wrap-up">📋 总结</h2>
<p>至此，我们已经成功制作了一个带有动画和材质的 Blender 模型。 然后我们使用 React Three Fiber 将它集成到我们的 React.js 应用程序中。</p>
<p>尽管我们在这里看到的示例非常基础，但对于更复杂的 Blender 模型，集成方法仍然相同。 Three.js 的基本功能可以与补充帮助程序相结合来增强场景。</p>
<p>要在 Three.js 场景中增强 Blender 模型的视觉效果，除了后期处理、附加动画或特定的 Blender 材质外，相机和灯光等方面往往是最重要的。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 TDD 和 React 测试库构建健壮的 React 应用程序 ]]>
                </title>
                <description>
                    <![CDATA[ 在我开始学习 React 时，我曾经挣扎于如何以一种既有用又直观的方式测试我的 web 应用。每次我想要测试一个组件时，我都会使用 Enzyme [http://airbnb.io/enzyme/docs/api/] 和 Jest [https://facebook.github.io/jest/]  进行表层渲染。 当然，我绝对是在滥用快照测试功能。 好吧，至少我写了一个测试，对吧？ 你可能在某个地方听说过，编写单元测试和集成测试将提高你编写的软件的质量。另一方面，糟糕的测试会滋生虚假的信心。 最近，我参加了 workshop.me [https://workshop.me/] 上由 Kent C. Dodds [https://www.freecodecamp.org/news/how-to-build-sturdy-react-apps-with-tdd-and-the-react-testing-library-47ad3c5c8e47/undefined]  主持的一个研讨会，他教我们如何为 React 应用编写更好的集成测试。 他还指导我们使用他的新的测试库 [h ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-build-sturdy-react-apps-with-tdd-and-the-react-testing-library/</link>
                <guid isPermaLink="false">64cba65db05f0606c70fb7c9</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 测试 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ herosql ]]>
                </dc:creator>
                <pubDate>Wed, 02 Aug 2023 13:10:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/08/1691068282139.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-build-sturdy-react-apps-with-tdd-and-the-react-testing-library-47ad3c5c8e47/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to build sturdy React apps with TDD and the React Testing Library</a>
      </p><!--kg-card-begin: markdown--><p>在我开始学习 React 时，我曾经挣扎于如何以一种既有用又直观的方式测试我的 web 应用。每次我想要测试一个组件时，我都会使用 <a href="http://airbnb.io/enzyme/docs/api/">Enzyme</a> 和 <a href="https://facebook.github.io/jest/">Jest</a> 进行表层渲染。</p>
<p>当然，我绝对是在滥用快照测试功能。</p>
<p>好吧，至少我写了一个测试，对吧？</p>
<p>你可能在某个地方听说过，编写单元测试和集成测试将提高你编写的软件的质量。另一方面，糟糕的测试会滋生虚假的信心。</p>
<p>最近，我参加了 <a href="https://workshop.me/">workshop.me</a> 上由 <a href="https://www.freecodecamp.org/news/how-to-build-sturdy-react-apps-with-tdd-and-the-react-testing-library-47ad3c5c8e47/undefined">Kent C. Dodds</a> 主持的一个研讨会，他教我们如何为 React 应用编写更好的集成测试。</p>
<p>他还指导我们使用他的<a href="https://github.com/kentcdodds/react-testing-library">新的测试库</a>，以强调以用户可能遇到的相同方式测试应用程序。</p>
<p>在本文中，我们将通过创建评论反馈来学习如何在构建稳定的 React 应用程序中运用 TDD。当然，这个过程适用于几乎所有的软件开发，而不仅仅是 React 或 JavaScript 应用。</p>
<h3 id="">开始</h3>
<p>我们将首先运行 <code>create-react-app</code> 并安装依赖项。我的假设是，如果你正在阅读关于测试应用程序的文章，你可能已经熟悉安装和启动 JavaScript 项目。在这里，我将使用 yarn 而不是 npm。</p>
<pre><code class="language-plain">create-react-app comment-feed
</code></pre>
<pre><code class="language-plain">cd comment-feed
</code></pre>
<pre><code class="language-plain">yarn
</code></pre>
<p>首先，我们将删除 src 目录中除 index.js 之外的所有文件。然后，在 src 文件夹内部，创建一个名为 components 的新文件夹和另一个名为 containers 的文件夹。</p>
<p>在测试工具方面，我将使用 Kent 的 <a href="https://github.com/kentcdodds/react-testing-library">React 测试库</a> 构建此应用程序。它是一款轻量级的测试工具，鼓励开发者以实际使用时相同的方式测试他们的应用程序。</p>
<p>与 Enzyme 一样，它导出一个渲染函数，但这个渲染函数始终对你的组件进行完整挂载。它导出辅助方法，允许你通过标签、文本甚至测试 ID 来定位元素。Enzyme 也通过其 <code>mount</code> API 实现了这一点，但它创建的抽象实际上提供了更多选项，其中许多选项允许你摆脱测试实现细节。</p>
<p>我们不想要测试所有的实现细节。我们想要渲染一个组件，看看当点击或更改 UI 上的某些内容时是否会发生正确的事情。就是这样！不再直接检查 props 、state 或类名。</p>
<p>现在让我们安装它们并开始工作。</p>
<pre><code class="language-plain">yarn add react-testing-library
</code></pre>
<h3 id="tdd">通过 TDD 构建评论反馈</h3>
<p>让我们以 TDD 风格进行第一个组件的开发。启动你的测试运行器。</p>
<pre><code class="language-plain">yarn test --watch
</code></pre>
<p>在 <code>containers</code> 文件夹中，我们将添加一个名为 CommentFeed.js 的文件。与之相伴的，添加一个名为 CommentFeed.test.js 的文件。在第一个测试中，让我们验证用户是否可以创建评论。太早了？好吧，既然我们还没有任何代码，我们将从一个较小的测试开始。检查一下是否可以渲染反馈。</p>
<h3 id="reacttestinglibrary">关于 react-testing-library 的一些说明</h3>
<p>首先，让我们注意这里的渲染函数。它类似于 <code>react-dom</code> 将组件渲染到 DOM 的方式，但它返回一个对象，我们可以解构该对象以获得一些实用的测试辅助工具。在这种情况下，我们得到 <code>queryByText</code>，它会返回我们期望在 DOM 上看到的 HTML 元素。</p>
<p><a href="https://github.com/kentcdodds/react-testing-library#faq">React 测试库文档</a>提供了一个层次结构，帮助你决定使用哪个查询或获取方法。通常，顺序如下：</p>
<ul>
<li><code>getByLabelText</code>（表单输入）</li>
<li><code>getByPlaceholderText</code>（仅在你的输入没有标签时使用 — 很少使用！）</li>
<li><code>getByText</code>（按钮和标题）</li>
<li><code>getByAltText</code>（图片）</li>
<li><code>getByTestId</code>（用于动态文本或其他你想要测试的奇怪元素）</li>
</ul>
<p>每个方法都有一个相关的 <code>queryByFoo</code>，除了在找不到元素时不会使测试失败之外，它们的功能相同。如果你只是测试元素的<strong>存在</strong>，请使用这些方法。</p>
<p>如果这些方法都无法满足你的需求，<code>render</code> 方法还返回映射到 <code>container</code> 属性的 DOM 元素，因此你可以像 <code>container.querySelector(‘body #root’)</code> 这样使用它。</p>
<h3 id="">首次实现代码</h3>
<p>现在，实现看起来相当简单。我们只需要确保“评论反馈”是一个组件。</p>
<p>它可能会更糟糕 - 我的意思是，我在编写整篇文章的过程中，还要编写组件的样式。幸运的是，测试并不太关心样式，所以我们可以专注于应用逻辑。</p>
<p>接下来的测试将验证我们是否可以渲染评论。但是我们甚至还没有任何评论，所以也添加一个组件，在测试之后添加。</p>
<p>我还将创建一个 props 对象来存储我们可能在这些测试中重用的数据。</p>
<p>在这种情况下，我正在检查评论的数量是否等于传入 CommentFeed 的项目数量。这是无关紧要的，但测试失败给了我们创建 Comment.js 文件的机会。</p>
<p>这为我们的测试套件亮起了绿灯，我们就可以放心地继续了。向 TDD 致敬。当我们给它一个空数组时，它当然会工作。但是，如果我们给它一些真实的对象，会发生什么呢？</p>
<p>我们必须更新实现以实际渲染内容。现在我们知道要去哪里，这很简单，对吧？</p>
<p>看看这个，我们的测试再次通过了。这是一个美妙的截图。</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*vGkFKnUkA9ms5PbaOWoQ_A.png" alt="1*vGkFKnUkA9ms5PbaOWoQ_A" width="800" height="458" loading="lazy"></p>
<p>注意我从未说过我们应该用 <code>yarn start</code> 启动程序吗？我们继续保持这种方式一段时间。关键是，你必须用心去感受代码。</p>
<p>样式只是外部表现 - 重要的是内部的东西。</p>
<p>以防你想启动应用程序，将 index.js 更新为以下内容：</p>
<h3 id="">添加评论表单</h3>
<p>这是事情开始变得更有趣的地方。这是我们从困倦地检查 DOM 节点的存在到实际使用它并<strong>验证行为</strong>的地方。所有其他的东西都是热身。</p>
<p>让我们从描述我想要的表单开始。它应该：</p>
<ul>
<li>包含一个作者的文本输入</li>
<li>包含一个评论条目的文本输入</li>
<li>有一个提交按钮</li>
<li>最终调用 API 或处理创建和存储评论的其他服务。</li>
</ul>
<p>我们可以在一个集成测试中完成这个列表。对于之前的测试用例，我们进行得相当缓慢，但现在我们要加快速度，一举完成。</p>
<p>注意我们的测试套件是如何发展的吗？我们从在各自的测试用例中硬编码 props 转变为为它们创建一个工厂。</p>
<h4 id="">准备、执行、断言</h4>
<p>以下集成测试可以分为三个部分：准备，执行和断言。</p>
<ul>
<li><strong>准备：</strong> 为测试用例创建props和其他测试用例</li>
<li><strong>执行：</strong> 模拟对元素的更改，例如文本输入或按钮点击</li>
<li><strong>断言：</strong>  断言所需的函数被正确次数调用，并使用正确的参数</li>
</ul>
<p>关于代码，我们做了一些假设，比如我们的标签命名或我们将拥有一个 <code>createComment</code> prop。</p>
<p>在查找输入时，我们希望尝试通过它们的标签找到它们。这样在构建应用程序时，可以优先考虑可访问性。使用 <code>container.querySelector</code> 是获取表单的最简单方法。</p>
<p>接下来，我们必须为输入分配新值，并模拟更改以更新它们的状态。这一步可能感觉有点奇怪，因为通常一次输入一个字符，为每个新字符更新组件的状态。</p>
<p>这个测试的行为更像是复制/粘贴的行为，从空字符串变为 “Socrates” 。目前没有中断问题，但我们可能需要注意一下，以防以后出现问题。</p>
<p>在提交表单后，我们可以对诸如调用了哪些 props 以及使用了哪些参数等事项进行断言。我们还可以利用这个时刻来验证表单输入是否已清除。</p>
<p>这让人望而生畏吗？不要害怕。首先将表单添加到渲染函数中。</p>
<p>我可以将这个表单分解成一个单独的组件，但现在我会保持不变。相反，我会将其添加到我桌子旁边的“重构愿望清单”中。</p>
<p>这是 TDD 的方式。当某件事看起来可以重构时，做个记录然后继续。只有在抽象的存在对你有益且不感到多余时才进行重构。</p>
<p>还记得我们通过创建 <code>createProps</code> 工厂来重构测试套件的时候吗？就像那样。我们也可以重构测试。</p>
<p>现在，让我们添加 <code>handleChange</code> 和 <code>handleSubmit</code> 类方法。当我们更改输入或提交表单时，这些方法会被触发。我还将初始化状态。</p>
<p>这样做就可以了。我们的测试通过了，我们有一些类似于真实应用程序的东西。我们的覆盖率看起来如何？</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*Q4coAIT2yaP120pDWGxoAQ.png" alt="1*Q4coAIT2yaP120pDWGxoAQ" width="800" height="564" loading="lazy"></p>
<p>还不错。如果我们忽略 index.js 中的所有设置，我们就有一个完全覆盖的 Web 应用程序，至少在执行的行数方面是这样。</p>
<p>当然，为了验证应用程序是否按照我们的意图工作，我们可能还需要测试其他用例。覆盖率的数字只是你的老板在谈论其他同事时可以炫耀的东西。</p>
<h3 id="">点赞评论</h3>
<p>如何检查我们可以点赞评论呢？这可能是在我们的应用程序中建立某种身份验证概念的好时机。但我们现在还不会跳得太远。首先，让我们更新 props 工厂，为生成的评论添加一个 <code>auth</code> 字段和 ID。</p>
<p>“认证”用户将通过应用程序传递其 <code>auth</code> 属性。与他们是否经过身份验证相关的任何操作都将被记录。</p>
<p>在许多应用程序中，此属性可能包含在向服务器发出请求时发送的某种访问令牌或 cookie 中。</p>
<p>在客户端，此属性的存在使应用程序知道它们可以让用户查看其个人资料或其他受保护的路由。</p>
<p>然而，在这个测试示例中，我们不会过多地处理身份验证。想象这样一个场景：当你进入聊天室时，你提供网名。从那时起，你将负责使用此网名的每个评论，尽管其他人也使用同样的名称登录。</p>
<p>虽然这不是一个很好的解决方案，即使在这个人为的例子中，我们只关心 CommentFeed 组件的行为是否符合预期。我们不关心用户<strong>如何</strong>登录。</p>
<p>换句话说，我们可能有一个完全不同的登录组件来处理特定用户的身份验证，以获得让用户在我们的应用程序中大显身手的全能 <code>auth</code> 属性。</p>
<p>让我们“喜欢”一条评论。添加下一个测试用例，然后更新 props 工厂以包含 <code>likeComment</code> 。</p>
<p>现在，对于实现，我们将首先更新 Comment 组件，使其具有一个点赞按钮以及一个 <code>data-testid</code> 属性，以便我们可以找到它。</p>
<p>我将测试 ID 直接放在按钮上，以便我们可以立即模拟对它的点击，而无需嵌套查询选择器。我还在按钮上附加了一个 onClick 处理程序，以便它传递给它的 onLike 函数。</p>
<p>现在我们只需将此类方法添加到我们的 CommentFeed。</p>
<p>你可能想知道为什么我们不直接将 <code>likeComment</code> 通过 prop 传递给 Comment 组件。为什么我们要将其作为类属性？</p>
<p>在这种情况下，因为它相当简单，我们不必构建这个抽象。将来，我们可能会决定添加其他 onClick 处理程序，例如处理分析事件或启动对该帖子评论的订阅。</p>
<p>在此容器组件的 <code>handleLike</code> 方法中捆绑多个不同的函数调用具有其优势。如果我们愿意，在成功“点赞”后，我们还可以使用此方法更新组件的状态。</p>
<h3 id="">不喜欢评论</h3>
<p>到目前为止，我们已经有了渲染、创建和喜欢评论的工作测试。当然，我们还没有实现实际执行此操作的逻辑——我们没有更新存储或写入数据库。</p>
<p>你可能还注意到，我们正在测试的逻辑很脆弱，不太适用于真实世界的评论反馈。例如，如果我们尝试喜欢我们已经喜欢的评论，会发生什么？它会无限地增加喜欢的计数，还是会取消喜欢？我可以喜欢我自己的评论吗？</p>
<p>我将把组件的功能扩展留给你的想象，但一个好的开始是编写一个新的测试用例。这里有一个基于我们想要实现不喜欢已经喜欢的评论的假设。</p>
<p>请注意，我们正在构建的评论反馈允许我喜欢我自己的评论。谁会这样做？</p>
<p>我已经更新了 Comment 组件，添加了一些逻辑来确定当前用户是否喜欢该评论。</p>
<p>好吧，我有点作弊：在我们之前将 <code>author</code> 传递给 <code>onLike</code> 函数的地方，我改为 <code>currentUser</code> ，这是传递给 Comment 组件的 <code>auth</code> 属性。</p>
<p>毕竟，当其他人喜欢他们的评论时，评论的作者出现在那里是没有意义的。</p>
<p>我之所以意识到这一点，是因为我一直在努力编写测试。如果我只是偶尔编写代码，这一点可能会被我忽略，直到我的一位同事斥责我的无知。</p>
<p>但是这里没有无知，只有测试和随之而来的代码。确保更新 CommentFeed，以便它期望传递 <code>auth</code> 属性。对于 <code>onClick</code> 处理程序，我们可以省略传递 <code>auth</code> 属性，因为我们可以从父级的 <code>handleLike</code> 和 <code>handleDislike</code> 方法中获取 <code>auth</code> 属性。</p>
<h3 id="">总结</h3>
<p>希望你的测试套件看起来像一棵未点亮的圣诞树。</p>
<p>我们可以采取很多不同的方法，这可能会让人有点不知所措。每当你想到某个想法时，只需将其写下来，无论是在纸上还是在新的测试块中。</p>
<p>例如，假设你希望在一个单独的类方法中实现 <code>handleLike</code> 和 <code>handleDislike</code>，但你现在有其他优先事项。你可以通过在测试用例中编写文档来实现这一点：</p>
<p>这并不意味着你需要编写一个全新的测试。你也可以更新前两个案例。但关键是，你可以将测试运行器用作应用程序更加紧迫的“待办事项“列表。</p>
<h4 id="">有用的链接</h4>
<p>有一些很棒的内容涉及到大规模的测试。以下是一些特别启发了本文以及我自己实践的内容。</p>
<ul>
<li><a href="https://www.freecodecamp.org/news/how-to-build-sturdy-react-apps-with-tdd-and-the-react-testing-library-47ad3c5c8e47/undefined">Kent C. Dodds</a> 编写的<a href="https://blog.kentcdodds.com/introducing-the-react-testing-library-e3a274307e65">介绍 React 测试库</a>。了解这个测试库背后的哲学是个好主意。</li>
<li>Kostis Kapelonis 写的<a href="http://blog.codepipes.com/testing/software-testing-antipatterns.html">软件测试反模式</a>，一篇非常深入的文章，讨论了单元和集成测试。还有如何避免错误的方法。</li>
<li>Kent Beck 写的<a href="https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530">测试驱动开发的示例</a>。这是一本讨论 TDD 模式的实体书。它不太长，写作风格通俗易懂，便于理解。</li>
</ul>
<p>我希望这些能帮你处理测试问题。</p>
<p>想了解更多文章或机智的评论吗？如果你喜欢这篇文章，请给我一些掌声，并在 <a href="http://medium%5D%28https//medium.com/@iwilsonq">Medium</a>、<a href="https://github.com/iwilsonq">Github</a> 和 <a href="https://twitter.com/iwilsonq">Twitter</a> 上关注我！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ React.useEffect Hook 常见问题及解决方法 ]]>
                </title>
                <description>
                    <![CDATA[ 大多数开发人员都非常熟悉 React hooks 的工作方式和常见用例，但是有一个 useEffect 问题很多人可能不太清楚。 用例 让我们从一个简单的场景开始。我们正在构建一个 React 应用程序，希望在一个组件中显示当前用户的用户名。但首先，我们需要从 API 中获取用户名。 因为我们知道我们也需要在应用程序的其他地方使用用户数据，所以我们还想在自定义 React hook 中抽象数据获取逻辑。 我们希望 React 组件看起来像这样： const Component = () => {   // useUser custom hook      return <div>{user.name}</div>; }; 看起来很简单！ useUser React hook 第二步是创建我们的 useUser 自定义钩子。 const useUser = (user) ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/most-common-react-useeffect-problems-and-how-to-fix-them/</link>
                <guid isPermaLink="false">616cd995d05b5a0660d4f0ff</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chengjun.L ]]>
                </dc:creator>
                <pubDate>Wed, 19 Jul 2023 07:18:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/11/react-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/most-common-react-useeffect-problems-and-how-to-fix-them/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">React.useEffect Hook – Common Problems and How to Fix Them</a>
      </p><p>大多数开发人员都非常熟悉 React hooks 的工作方式和常见用例，但是有一个 <code>useEffect</code> 问题很多人可能不太清楚。</p><h2 id="-">用例</h2><p>让我们从一个简单的场景开始。我们正在构建一个 React 应用程序，希望在一个组件中显示当前用户的用户名。但首先，我们需要从 API 中获取用户名。</p><p>因为我们知道我们也需要在应用程序的其他地方使用用户数据，所以我们还想在自定义 React hook 中抽象数据获取逻辑。</p><p>我们希望 React 组件看起来像这样：</p><pre><code>const Component = () =&gt; {
  // useUser custom hook
  
  return &lt;div&gt;{user.name}&lt;/div&gt;;
};
</code></pre><p>看起来很简单！</p><h1 id="useuser-react-hook"><strong>useUser React hook</strong></h1><p>第二步是创建我们的 <code>useUser</code> 自定义钩子。</p><pre><code>const useUser = (user) =&gt; {
  const [userData, setUserData] = useState();
  useEffect(() =&gt; {
    if (user) {
      fetch("users.json").then((response) =&gt;
        response.json().then((users) =&gt; {
          return setUserData(users.find((item) =&gt; item.id === user.id));
        })
      );
    }
  }, []);

  return userData;
};
</code></pre><p>让我们分解一下。我们正在检查钩子是否正在接收用户对象。之后，我们从名为 <code>users.json</code> 的文件中获取用户列表，并对其进行过滤以找到具有我们需要的 id 的用户。</p><p>然后，一旦我们获得了必要的数据，我们就将其保存为钩子的 <code>userData</code> 状态。最后返回 <code>userData</code>。</p><blockquote><strong>注意：</strong>这是一个仅用于说明目的的示例！现实世界中的数据获取要复杂得多。如果你对该主题感兴趣，请查看我关于如何使用 ReactQuery、Typescript 和 GraphQL 创建出色的数据获取设置的<a href="https://blog.whereisthemouse.com/graphql-requests-made-easy-with-react-query-and-typescript">文章</a>。</blockquote><p>让我们在 React 组件中插入钩子，看看会发生什么。</p><pre><code>const Component = () =&gt; {
  const user = useUser({ id: 1 });
  return &lt;div&gt;{user?.name}&lt;/div&gt;;
};
</code></pre><p>好的，一切都按预期进行。但是等等……这是什么？</p><h1 id="eslint-exhaustive-deps-"><strong>ESLint exhaustive-deps 规则</strong></h1><p>我们的钩子中有一个 ESLint 警告：</p><pre><code>React Hook useEffect has a missing dependency: 'user'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
</code></pre><p>嗯，我们的 <code>useEffect</code> 似乎缺少依赖项。那好吧！ 让我们添加它。可能发生的最坏情况是什么？ 😂</p><pre><code>const useUser = (user) =&gt; {
  const [userData, setUserData] = useState();
  useEffect(() =&gt; {
    if (user) {
      fetch("users.json").then((response) =&gt;
        response.json().then((users) =&gt; {
          return setUserData(users.find((item) =&gt; item.id === user.id));
        })
      );
    }
  }, [user]);

  return userData;
};
</code></pre><p>看起来我们的组件 <code>Component</code> 现在不会停止重新渲染。这里发生了什么？！</p><p>让我们解释一下。</p><h1 id="--1">无限重新渲染问题</h1><p>我们的组件重新渲染的原因是因为 <code>useEffect</code> 依赖项在不断变化。但为什么？我们总是将相同的对象传递给钩子！</p><p>虽然我们确实传递了一个具有相同键和值的对象，但它并不是完全相同的对象。每次重新渲染组件时，我们实际上都是在创建一个新对象，然后我们将新对象作为参数传递给 <code>useUser</code> 钩子。</p><p>在内部，<code>useEffect</code> 比较两个对象，由于它们有不同的引用，它再次获取用户并将新的用户对象设置为状态。状态更新然后触发组件中的重新渲染，不断重复……</p><p>所以，我们能做些什么？</p><h1 id="--2">如何修复</h1><p>现在我们了解了问题，可以开始寻找解决方案。</p><p>第一个也是最明显的选择是从 <code>useEffect</code> 依赖数组中移除依赖，忽略 ESLint 规则，继续我们的生活。</p><p>但这是错误的做法。它可以（并且可能会）导致我们的应用程序中出现错误和意外行为。如果你想了解更多有关 <code>useEffect</code> 如何工作的信息，我强烈推荐 Dan Abramov 的<a href="https://overreacted.io/a-complete-guide-to-useeffect/">完整指南</a>。</p><p>下一个是什么？</p><p>在我们的例子中，最简单的解决方案是从组件中取出 <code>{ id: 1 }</code> 对象。这将为对象提供稳定的引用并解决我们的问题。</p><pre><code>const userObject = { id: 1 };

const Component = () =&gt; {
  const user = useUser(userObject);
  return &lt;div&gt;{user?.name}&lt;/div&gt;;
};

export default Component;
</code></pre><p>但这并不总是可能的。想象一下，用户 id 以某种方式依赖于组件 props 或状态。</p><p>例如，可能是我们使用 URL 参数来访问它。如果是这种情况，我们可以使用一个方便的 <code>useMemo</code> 钩子来记忆对象并再次确保稳定的引用。</p><pre><code>const Component = () =&gt; {
  const { userId } = useParams();
  
  const userObject = useMemo(() =&gt; {
    return { id: userId };
  }, [userId]); // Don't forget the dependencies here either!

  const user = useUser(userObject);
  return &lt;div&gt;{user?.name}&lt;/div&gt;;
};

export default Component;
</code></pre><p>最后，不是将对象变量传递给我们的 <code>useUser</code> 钩子，而是可以只传递用户 ID 本身，这是一个原始值。这将防止 <code>useEffect</code> 钩子中的引用相等问题。</p><pre><code>const useUser = (userId) =&gt; {
  const [userData, setUserData] = useState();

  useEffect(() =&gt; {
    fetch("users.json").then((response) =&gt;
      response.json().then((users) =&gt; {
        return setUserData(users.find((item) =&gt; item.id === userId));
      })
    );
  }, [userId]);

  return userData;
};

const Component = () =&gt; {
  const user = useUser(1);

  return &lt;div&gt;{user?.name}&lt;/div&gt;;
};
</code></pre><p>问题解决啦！</p><p>在此过程中，我们甚至不必违反任何 ESLint 规则......</p><p><em><strong>注意：</strong>如果我们传递给自定义钩子的参数是一个函数，而不是一个对象，我们将使用非常相似的技术来避免无限重新渲染。一个显着的区别是我们必须在上面的例子中用 <code>useCallback</code> 替换 <code>useMemo</code>。</em></p><p>感谢你阅读本文！</p><p>对代码感到好奇吗？在<a href="https://codesandbox.io/s/useeffect-gotcha-20jw9?file=/src/App.js">这里</a>探索吧。</p><p>欢迎访问我的<a href="https://blog.whereisthemouse.com/">博客</a>并在 <a href="https://twitter.com/iva_kop">Twitter</a> 上关注我，以获取更多与 React 相关的内容。</p><p>图片作者：<a href="https://www.freepik.com/vectors/technology">vectorjuice</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 React、React Hooks 和 Axios 进行 CRUD 操作 ]]>
                </title>
                <description>
                    <![CDATA[ 如果你正在使用React，理解和实现API请求可能是相当困难的。 所以在这篇文章中，我们将通过使用React、React Hooks、React Router和Axios实现CRUD操作来学习这一切。 让我们深入了解一下。 如何安装Node和npm 首先，让我们在系统中安装Node。我们将主要用它来执行我们的JavaScript代码。 去官方网站下载Node，https://nodejs.org/en/。 你还需要node包管理器 ，如npm，它是内建在Node上的。你可以用它来为你的JavaScript应用程序安装包。幸运的是，Node自带npm，所以你不需要单独下载它。 一旦它们都完成了，打开你的终端或命令提示符，输入node -v。这将检查你有哪个版本的Node。 如何创建你的React应用 为了创建你的React应用，在终端输入 npx create-react-app <你的应用程序名称>， 或者本例中的**npx create-react-app**react-crud**。 你会看到软件包正在被安装。 一旦软件包安装完毕，进入项目文件夹，输入npm st ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-perform-crud-operations-using-react/</link>
                <guid isPermaLink="false">645859f60f634b0716652bb9</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React Hooks ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Axios ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Fri, 05 May 2023 11:10:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/05/React-CRUD-Operations-using-React-and-React-Hooks-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-perform-crud-operations-using-react/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Perform CRUD Operations using React, React Hooks, and Axios</a>
      </p><!--kg-card-begin: markdown--><p>如果你正在使用React，理解和实现API请求可能是相当困难的。</p>
<p>所以在这篇文章中，我们将通过使用React、React Hooks、React Router和Axios实现CRUD操作来学习这一切。</p>
<p>让我们深入了解一下。</p>
<h2 id="nodenpm">如何安装Node和npm</h2>
<p>首先，让我们在系统中安装Node。我们将主要用它来执行我们的JavaScript代码。</p>
<p>去官方网站下载Node，<a href="https://nodejs.org/en/">https://nodejs.org/en/</a>。</p>
<p>你还需要<strong>node包管理器</strong>，如npm，它是内建在Node上的。你可以用它来为你的JavaScript应用程序安装包。幸运的是，Node自带npm，所以你不需要单独下载它。</p>
<p>一旦它们都完成了，打开你的终端或命令提示符，输入<code>node -v</code>。这将检查你有哪个版本的Node。</p>
<h2 id="react">如何创建你的React应用</h2>
<p>为了创建你的React应用，在终端输入 <strong><strong><code>npx create-react-app &lt;你的应用程序名称&gt;</code></strong></strong>， 或者本例中的**<code>npx create-react-app**react-crud</code>**。</p>
<p>你会看到软件包正在被安装。</p>
<p>一旦软件包安装完毕，进入项目文件夹，输入<code>npm start</code>。</p>
<p>你会看到默认的React模板，像这样：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-124754.png" alt="默认的 React Boilerplate 模板" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>默认的 React Boilerplate 模板</figcaption>
</figure>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-124858.png" alt="我们的 App.js文件" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>我们的 App.js文件</figcaption>
</figure>
<h2 id="reactsemanticui">如何为React安装Semantic UI包（库）</h2>
<p>让我们在我们的项目中安装Semantic UI React软件包。Semantic UI是一个为React制作的UI库，它有预建的UI组件，比如表格、按钮和许多功能。</p>
<p>你可以使用下面的一个命令来安装它，这取决于你的包管理器。</p>
<pre><code class="language-bash">yarn add semantic-ui-react semantic-ui-css
</code></pre>
<p>对于使用 Yarn 包管理器</p>
<pre><code class="language-bash">npm install semantic-ui-react semantic-ui-css
</code></pre>
<p>对于使用 NPM 包管理器</p>
<p>同时，在你的应用程序的主入口文件中导入该库，也就是index.js。</p>
<pre><code class="language-js">import 'semantic-ui-css/semantic.min.css'
</code></pre>
<p>在你的index.js文件中粘贴上面一行内容。</p>
<h2 id="crud"><strong>如何构建你的CRUD应用</strong></h2>
<p>现在，让我们开始使用React构建我们的CRUD应用。</p>
<p>首先，我们要给我们的应用程序添加一个标题。</p>
<p>在我们的app.js文件中，添加一个标题，像这样：</p>
<pre><code>import './App.css';

function App() {
  return (
    &lt;div&gt;
      React Crud Operations
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>在我们的应用程序中添加一个标题</p>
<p>现在，让我们确保它居中。</p>
<p>给父级div一个classname，即main。在App.css文件中，我们将使用Flexbox来使标题居中。</p>
<pre><code>import './App.css';

function App() {
  return (
    &lt;div className="main"&gt;
      React Crud Operations
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>app.js文件，在父 div 中的 className 为main的css定义。</p>
<pre><code>.main{
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}
</code></pre>
<p>我们的 app.css 文件</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-130340.png" alt="Screenshot-2021-07-24-130340" width="600" height="400" loading="lazy"></p>
<p>现在我们的标题已经完全居中了。</p>
<p>为了让它看起来更好一些，我们需要使它加粗，并添加一些很酷的字体。要做到这一点，我们将在我们的标题周围使用标题标签，像这样：</p>
<pre><code>import './App.css';

function App() {
  return (
    &lt;div className="main"&gt;
      &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>让我们从 Google Font导入一种字体. 从 <a href="https://fonts.google.com/">https://fonts.google.com/</a>选择一种。</p>
<p>选择任何你喜欢的字体，但我将使用Montserrat字体家族。</p>
<p>在App.css文件中导入你选择的字体，像这样：</p>
<pre><code>@import url('https://fonts.googleapis.com/css2?family=Montserrat&amp;display=swap');
</code></pre>
<p>现在，让我们改变标题的字体。</p>
<pre><code>&lt;div className="main"&gt;
      &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
    &lt;/div&gt;
</code></pre>
<p>给 <code>h2</code>一个 <code>lassName</code> 为 <code>main-header</code>，就像上面。</p>
<p>然后，在你的 App.css文件，添加font family：</p>
<pre><code>.main-header{
  font-family: 'Montserrat', sans-serif;
}
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-132749.png" alt="Screenshot-2021-07-24-132749" width="600" height="400" loading="lazy"></p>
<p>现在你会看到改变后的标题。</p>
<h2 id="crud">如何创建你的CRUD组件</h2>
<p>让我们创建四个CRUD组件，它们将是创建、读取、更新和删除。</p>
<p>在我们的src文件夹中，创建一个名为组件的文件夹。在这个文件夹中，创建三个文件--创建、读取和更新。对于删除，我们不需要任何额外的组件。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133242.png" alt="Screenshot-2021-07-24-133242" width="600" height="400" loading="lazy"></p>
<p>现在，让我们来实现这些。</p>
<p>但为此，我们需要使用Mock API。这些API将向我们将要创建的假服务器发送数据，这只是为了学习的目的。</p>
<p>所以，请前往 <a href="https://mockapi.io/">https://mockapi.io/</a> ，创建账号。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133456.png" alt="MockAPI" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>MockAPI</figcaption>
</figure>
<p>通过点击<code>(plus)加号</code>按钮创建一个项目。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133553.png" alt="Screenshot-2021-07-24-133553" width="600" height="400" loading="lazy"></p>
<p>点击<code>(plus)加号</code>按钮，创建一个新的项目。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133702.png" alt="Screenshot-2021-07-24-133702" width="600" height="400" loading="lazy"></p>
<p>添加你的项目名称，然后点击<code>(create)创建</code>按钮。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133748.png" alt="Screenshot-2021-07-24-133748" width="600" height="400" loading="lazy"></p>
<p>现在，通过点击 <code>(NEW RESOURCE)新资源</code> 按钮创建一个新资源。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133847.png" alt="Screenshot-2021-07-24-133847" width="600" height="400" loading="lazy"></p>
<p>它将要求你提供<code>(RESOURCE name)资源名称</code>，所以输入你想使用的名称。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-134009.png" alt="Screenshot-2021-07-24-134009" width="600" height="400" loading="lazy"></p>
<p>删除额外的字段，<code>如姓名(name)、头像(avatar)或创建时间(createdAt）</code>，因为我们将不需要这些。然后，点击<code>创建(create)</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-134140.png" alt="Screenshot-2021-07-24-134140" width="600" height="400" loading="lazy"></p>
<p>现在，我们已经创建了我们的 <code>fake API(假API)</code>，我把它命名为fakeData。</p>
<p>点击fakeData，你会看到API在一个新的标签中打开。现在的数据库是空的。</p>
<h2 id="createcomponent">如何为(create Component)创建组件创建一个表格</h2>
<p>让我们使用Semantic UI库中的一个表单。</p>
<p>前往Semantic React，在左边的搜索栏中搜索Form。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-134532.png" alt="Screenshot-2021-07-24-134532" width="600" height="400" loading="lazy"></p>
<p>你会看到一个像这样的表格，点击左上方的 <code>试试(Try it)</code>，就可以得到代码。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-134654.png" alt="Screenshot-2021-07-24-134654" width="600" height="400" loading="lazy"></p>
<p>复制这段代码并将其粘贴到你的Create.js文件中，像这样：</p>
<pre><code>import React from 'react'
import { Button, Checkbox, Form } from 'semantic-ui-react'

const Create = () =&gt; (
    &lt;Form&gt;
        &lt;Form.Field&gt;
            &lt;label&gt;First Name&lt;/label&gt;
            &lt;input placeholder='First Name' /&gt;
        &lt;/Form.Field&gt;
        &lt;Form.Field&gt;
            &lt;label&gt;Last Name&lt;/label&gt;
            &lt;input placeholder='Last Name' /&gt;
        &lt;/Form.Field&gt;
        &lt;Form.Field&gt;
            &lt;Checkbox label='I agree to the Terms and Conditions' /&gt;
        &lt;/Form.Field&gt;
        &lt;Button type='submit'&gt;Submit&lt;/Button&gt;
    &lt;/Form&gt;
)

export default Create;
</code></pre>
<p>在你的app.js文件中导入创建组件(Create Component)。</p>
<pre><code>import './App.css';
import Create from './components/create';

function App() {
  return (
    &lt;div className="main"&gt;
      &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
      &lt;div&gt;
        &lt;Create/&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>就像这样：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-135249.png" alt="Screenshot-2021-07-24-135249" width="600" height="400" loading="lazy"></p>
<p>但这里有一个问题--项目没有正确对齐，文本输入标签颜色是黑色的。所以，让我们来改变它。</p>
<p>在create.js文件中，给<strong>Form</strong>一个<code>create-form</code>的className。</p>
<pre><code>import React from 'react'
import { Button, Checkbox, Form } from 'semantic-ui-react'

const Create = () =&gt; (
    &lt;Form className="create-form"&gt;
        &lt;Form.Field&gt;
            &lt;label&gt;First Name&lt;/label&gt;
            &lt;input placeholder='First Name' /&gt;
        &lt;/Form.Field&gt;
        &lt;Form.Field&gt;
            &lt;label&gt;Last Name&lt;/label&gt;
            &lt;input placeholder='Last Name' /&gt;
        &lt;/Form.Field&gt;
        &lt;Form.Field&gt;
            &lt;Checkbox label='I agree to the Terms and Conditions' /&gt;
        &lt;/Form.Field&gt;
        &lt;Button type='submit'&gt;Submit&lt;/Button&gt;
    &lt;/Form&gt;
)

export default Create;
</code></pre>
<p>app.js</p>
<p>并在你的App.css文件中添加以下类。</p>
<pre><code>.create-form label{
  color: whitesmoke !important;
  font-family: 'Montserrat', sans-serif;
  font-size: 12px !important;
}
</code></pre>
<p>App.css</p>
<p>这个类将针对所有的表格字段标签并应用白烟的颜色。它还将改变字体并增加字体大小。</p>
<p>现在，在我们的主<code>className</code>中，添加一个flex-direction属性。这个属性将设置方向为列，所以主<code>className</code>中的每个元素都将水平对齐。</p>
<pre><code>.main{
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #212121;
  color: whitesmoke;
  flex-direction: column;
}
</code></pre>
<p>App.css</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-140141.png" alt="Screenshot-2021-07-24-140141" width="600" height="400" loading="lazy"></p>
<p>你可以看到，我们的表单现在看起来好多了。</p>
<p>接下来，让我们从表单字段中获取数据到我们的控制台(console)。为此，我们将使用React的<code>useState</code>钩子。</p>
<p>在我们的create.js文件中，从React中导入<code>useState</code>。</p>
<pre><code>import React, { useState } from 'react';
</code></pre>
<p>然后，为名字(first name)、姓氏(last name)和复选框(checkbox)创建状态。我们将这些状态初始化为空或假。</p>
<pre><code>import React, { useState } from 'react';
import { Button, Checkbox, Form } from 'semantic-ui-react'

export default function Create() {
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [checkbox, setCheckbox] = useState(false);
    return (
        &lt;div&gt;
            &lt;Form className="create-form"&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;First Name&lt;/label&gt;
                    &lt;input placeholder='First Name' /&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;Last Name&lt;/label&gt;
                    &lt;input placeholder='Last Name' /&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;Checkbox label='I agree to the Terms and Conditions' /&gt;
                &lt;/Form.Field&gt;
                &lt;Button type='submit'&gt;Submit&lt;/Button&gt;
            &lt;/Form&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>你可以看到，这现在是作为一个函数组件。所以，我们需要把这个组件改成一个函数组件。这是因为我们只能在函数组件中使用钩子。</p>
<p>现在，让我们分别使用<code>setFirstName</code>、<code>setLastName</code>和<code>setCheckbox</code>属性来设置名字、姓氏和复选框。</p>
<pre><code>&lt;input placeholder='First Name' onChange={(e) =&gt; setFirstName(e.target.value)}/&gt;

&lt;input placeholder='Last Name' onChange={(e) =&gt; setLastName(e.target.value)}/&gt;

&lt;Checkbox label='I agree to the Terms and Conditions' onChange={(e) =&gt; setCheckbox(!checkbox)}/&gt;
</code></pre>
<p>我们正在获得名字(first name)、姓氏(last name)和复选框(checkout)的状态。</p>
<p>创建一个名为<code>postData</code>的函数，我们将用它来向API发送数据。在该函数中，写下这段代码。</p>
<pre><code>const postData = () =&gt; {
        console.log(firstName);
        console.log(lastName);
        console.log(checkbox);
}
</code></pre>
<p>我们在控制台中打印出名字(firstName)、姓氏(lastName)和复选框(checkbox)的值。</p>
<p>在(Submit button)提交按钮上，使用onClick事件调用这个函数，这样，每当我们按下提交按钮，这个函数就会被调用。</p>
<pre><code>&lt;Button onClick={postData} type='submit'&gt;Submit&lt;/Button&gt;
</code></pre>
<p>这里是 <em>create</em> 文件的全部代码。</p>
<pre><code>import React, { useState } from 'react';
import { Button, Checkbox, Form } from 'semantic-ui-react'

export default function Create() {
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [checkbox, setCheckbox] = useState(false);
    const postData = () =&gt; {
        console.log(firstName);
        console.log(lastName);
        console.log(checkbox);
    }
    return (
        &lt;div&gt;
            &lt;Form className="create-form"&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;First Name&lt;/label&gt;
                    &lt;input placeholder='First Name' onChange={(e) =&gt; setFirstName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;Last Name&lt;/label&gt;
                    &lt;input placeholder='Last Name' onChange={(e) =&gt; setLastName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;Checkbox label='I agree to the Terms and Conditions' onChange={(e) =&gt; setCheckbox(!checkbox)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Button onClick={postData} type='submit'&gt;Submit&lt;/Button&gt;
            &lt;/Form&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>在名字和姓氏中输入一些数值，并勾选复选框。然后，点击提交按钮。你会看到控制台中打印出的数据是这样的。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-142717.png" alt="Screenshot-2021-07-24-142717" width="600" height="400" loading="lazy"></p>
<h2 id="axiosmockapis">如何使用Axios向Mock APIs发送请求</h2>
<p>让我们使用Axios来发送我们的表单数据到模拟服务器。</p>
<p>但首先，我们需要安装它。</p>
<p>只要输入<code>npm i axios</code>来安装这个包。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-174213.png" alt="Screenshot-2021-07-24-174213" width="600" height="400" loading="lazy"></p>
<p>软件包安装完毕后，让我们开始(create)创建操作。</p>
<p>在文件的顶部导入Axios。</p>
<pre><code>import axios from 'axios';
</code></pre>
<p>导入Axios</p>
<p>在<code>postData</code>函数中，我们将使用Axios来发送POST请求。</p>
<pre><code>const postData = () =&gt; {
        axios.post(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData`, {
            firstName,
            lastName,
            checkbox
        })
    }
</code></pre>
<p>发送 Post 请求</p>
<p>如你所见，我们正在使用axios.post。在axios.post中, 我们有 API endpoint(接入点 请求地址), 这是我们之前创建的。然后，我们有被大括号包裹的表单字段。</p>
<p>当我们点击提交时，这个函数将被调用，它将向API服务器发布数据。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-174834.png" alt="Screenshot-2021-07-24-174834" width="600" height="400" loading="lazy"></p>
<p>输入你的名字(first name)，姓氏(last name)，并勾选复选框。点击提交。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-174930.png" alt="Screenshot-2021-07-24-174930" width="600" height="400" loading="lazy"></p>
<p>然后，你检查这个API的返回值，你会得到你的名字、姓氏，复选框为真的值，被包裹在一个对象中。</p>
<h2 id="">如何实现读取和更新操作</h2>
<p>为了开始(read)读取操作，我们需要创建一个读取页面。我们还需要React Router包来导航到不同的页面。</p>
<p>前往<a href="https://reactrouter.com/web/guides/quick-start">https://reactrouter.com/web/guides/quick-start</a>查看文档，同时运行 <code>npm i react-router-dom</code>进行安装。</p>
<p>安装完毕后，从React Router导入一些东西：</p>
<pre><code>import { BrowserRouter as Router, Route } from 'react-router-dom'
</code></pre>
<p>从<code>React Router</code>中导入<code>Router</code>和<code>Route</code>。</p>
<p>在我们的App.js中，把整个返回包成一个Router。这基本上意味着，无论这个Router里面有什么，都能在React中使用。</p>
<pre><code>import './App.css';
import Create from './components/create';
import { BrowserRouter as Router, Route } from 'react-router-dom'

function App() {
  return (
    &lt;Router&gt;
      &lt;div className="main"&gt;
        &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
        &lt;div&gt;
          &lt;Create /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/Router&gt;
  );
}

export default App;
</code></pre>
<p>我们的App.js现在看起来会像上面的样子。</p>
<p>替换掉返回里面的Create，并添加以下代码。</p>
<pre><code>import './App.css';
import Create from './components/create';
import { BrowserRouter as Router, Route } from 'react-router-dom'

function App() {
  return (
    &lt;Router&gt;
      &lt;div className="main"&gt;
        &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
        &lt;div&gt;
          &lt;Route exact path='/create' component={Create} /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/Router&gt;
  );
}

export default App;
</code></pre>
<p>在这里，我们使用Route组件作为Create。我们已经将Create的路径设置为'/create'。因此，如果我们进入<a href="http://localhost:3000/create">http://localhost:3000/create</a> ，我们将看到创建页面。</p>
<p>同样地，我们需要(read)读取和(update)更新的路由。</p>
<pre><code>import './App.css';
import Create from './components/create';
import Read from './components/read';
import Update from './components/update';
import { BrowserRouter as Router, Route } from 'react-router-dom'

function App() {
  return (
    &lt;Router&gt;
      &lt;div className="main"&gt;
        &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
        &lt;div&gt;
          &lt;Route exact path='/create' component={Create} /&gt;
        &lt;/div&gt;
        &lt;div style={{ marginTop: 20 }}&gt;
          &lt;Route exact path='/read' component={Read} /&gt;
        &lt;/div&gt;

        &lt;Route path='/update' component={Update} /&gt;
      &lt;/div&gt;
    &lt;/Router&gt;
  );
}

export default App;
</code></pre>
<p>因此，(read)读取和(update)更新路由，类似你上面看到的。</p>
<p>如果你前往 <a href="http://localhost:3000/read">http://localhost:3000/read</a>，会看到下面的：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-180318.png" alt="Read Route" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Read Route</figcaption>
</figure>
<p>在<a href="http://localhost:3000/update">http://localhost:3000/update</a> 网址，我们可以看到更新组件（Update Component），像这样：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-180440.png" alt="Screenshot-2021-07-24-180440" width="600" height="400" loading="lazy"></p>
<h3 id="">读取操作</h3>
<p>对于读取操作，我们将需要一个表组件。因此，前往React Semantic UI，并使用库中的一个表。</p>
<pre><code>import React from 'react';
import { Table } from 'semantic-ui-react'
export default function Read() {
    return (
        &lt;div&gt;
            &lt;Table singleLine&gt;
                &lt;Table.Header&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.HeaderCell&gt;Name&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Registration Date&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;E-mail address&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Premium Plan&lt;/Table.HeaderCell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Header&gt;

                &lt;Table.Body&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.Cell&gt;John Lilki&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;September 14, 2013&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;jhlilk22@yahoo.com&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;No&lt;/Table.Cell&gt;
                    &lt;/Table.Row&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.Cell&gt;Jamie Harington&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;January 11, 2014&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;jamieharingonton@yahoo.com&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;Yes&lt;/Table.Cell&gt;
                    &lt;/Table.Row&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.Cell&gt;Jill Lewis&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;May 11, 2014&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;jilsewris22@yahoo.com&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;Yes&lt;/Table.Cell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Body&gt;
            &lt;/Table&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Read.js</p>
<p>在这里，你可以看到我们有一个带有一些假数据(dummy data)的表。但我们只需要一个表行。所以，让我们删除其他的。</p>
<pre><code>import React from 'react';
import { Table } from 'semantic-ui-react'
export default function Read() {
    return (
        &lt;div&gt;
            &lt;Table singleLine&gt;
                &lt;Table.Header&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.HeaderCell&gt;Name&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Registration Date&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;E-mail address&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Premium Plan&lt;/Table.HeaderCell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Header&gt;

                &lt;Table.Body&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.Cell&gt;John Lilki&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;September 14, 2013&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;jhlilk22@yahoo.com&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;No&lt;/Table.Cell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Body&gt;
            &lt;/Table&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Read.js</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-182905.png" alt="Screenshot-2021-07-24-182905" width="600" height="400" loading="lazy"></p>
<p>这是“阅读”页面的输出。我们有一个有四列的表，但我们只需要三列。</p>
<p>删除多余的字段列，并像这样重新命名字段。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-183105.png" alt="Screenshot-2021-07-24-183105" width="600" height="400" loading="lazy"></p>
<p>这就是我们的阅读页面现在的样子：</p>
<pre><code>import React from 'react';
import { Table } from 'semantic-ui-react'
export default function Read() {
    return (
        &lt;div&gt;
            &lt;Table singleLine&gt;
                &lt;Table.Header&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.HeaderCell&gt;First Name&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Last Name&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Checked&lt;/Table.HeaderCell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Header&gt;

                &lt;Table.Body&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.Cell&gt;Nishant&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;Kumar&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;Yes&lt;/Table.Cell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Body&gt;
            &lt;/Table&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Read.js</p>
<p>现在，让我们发送GET请求，从API获得数据。</p>
<p>当我们的应用程序加载时，我们需要这些数据。所以，我们要使用<code>useEffect</code>钩子(hook)。</p>
<pre><code>import React, { useEffect } from 'react';

 useEffect(() =&gt; {
       
 }, [])
</code></pre>
<p>useEffect钩子(hook)</p>
<p>创建一个包含传入数据的状态。这将是一个数组。</p>
<pre><code>import React, { useEffect, useState } from 'react';

const [APIData, setAPIData] = useState([]);
useEffect(() =&gt; {
       
}, [])
</code></pre>
<p>APIData state 来存储API传入的数据</p>
<p>在<code>useEffect</code>钩子(hook)中，让我们发送GET请求。</p>
<pre><code> useEffect(() =&gt; {
        axios.get(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData`)
            .then((response) =&gt; {
                setAPIData(response.data);
            })
    }, [])
</code></pre>
<p>因此，我们使用axios.get来向API发送GET请求。然后，如果请求被满足，我们就在我们的_APIData_状态中设置响应数据。</p>
<p>现在，让我们根据API数据来映射我们的表行。</p>
<p>我们将使用Map函数来做这件事。它将对数组进行迭代，并在输出中显示数据。</p>
<pre><code>&lt;Table.Body&gt;
  {APIData.map((data) =&gt; {
     return (
       &lt;Table.Row&gt;
          &lt;Table.Cell&gt;{data.firstName}&lt;/Table.Cell&gt;
           &lt;Table.Cell&gt;{data.lastName}&lt;/Table.Cell&gt;
           &lt;Table.Cell&gt;{data.checkbox ? 'Checked' : 'Unchecked'}&lt;/Table.Cell&gt;
        &lt;/Table.Row&gt;
   )})}
&lt;/Table.Body&gt;
</code></pre>
<p>我们根据API中的数据来映射firstName、lastName和checkbox。但我们的复选框有一点不同。我在这里使用了一个三元操作符（'?'）。如果data.checkbox为真，输出将是Checked，否则将是Unchecked。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-184955.png" alt="Read.js 输出" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Read.js 输出</figcaption>
</figure>
<h3 id="update">更新(Update)操作</h3>
<p>再为更新创建一个标题，并在表行中为更新按钮创建一列。使用Semantic UI React的按钮。</p>
<pre><code>&lt;Table.HeaderCell&gt;Update&lt;/Table.HeaderCell&gt;

&lt;Table.Cell&gt; 
  &lt;Button&gt;Update&lt;/Button&gt;
&lt;/Table.Cell&gt;
</code></pre>
<p>创建(Update)更新按钮</p>
<p>现在，当我们点击这个按钮，我们应该被重定向到更新页面。为此，我们需要React Router的链接。</p>
<p>从React Router导入Link。并将更新按钮的表格单元格包装成Link标签。</p>
<pre><code>import { Link } from 'react-router-dom';

&lt;Link to='/update'&gt;
  &lt;Table.Cell&gt; 
     &lt;Button&gt;Update&lt;/Button&gt;
   &lt;/Table.Cell&gt;
&lt;/Link&gt;
</code></pre>
<p>为更新按钮添加链接(Link)</p>
<p>因此，如果我们点击更新按钮，我们将被重定向到更新页面。</p>
<p>为了更新列的数据，我们需要它们各自的ID，这从APIs获得。</p>
<p>创建一个名为 "setData "的函数。将其绑定到更新按钮上。</p>
<pre><code> &lt;Button onClick={() =&gt; setData()}&gt;Update&lt;/Button&gt;
</code></pre>
<p>现在，我们需要将数据作为参数传递给上面的函数。</p>
<pre><code> &lt;Button onClick={() =&gt; setData(data)}&gt;Update&lt;/Button&gt;
</code></pre>
<p>并在上面的的函数中，在控制台中打印这些数据。</p>
<pre><code>const setData = (data) =&gt; {
   console.log(data);
}
</code></pre>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-190515.png" alt="控制台的数据" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>控制台的数据</figcaption>
</figure>
<p>点击表中的更新按钮，并查看控制台。你会得到相应表字段的数据。</p>
<p>让我们把这些数据设置到localStorage中。</p>
<pre><code>const setData = (data) =&gt; {
        let { id, firstName, lastName, checkbox } = data;
        localStorage.setItem('ID', id);
        localStorage.setItem('First Name', firstName);
        localStorage.setItem('Last Name', lastName);
        localStorage.setItem('Checkbox Value', checkbox)
}
</code></pre>
<p>在本地存储中(Local Storage)设置数据</p>
<p>我们正在将我们的数据解构为id、firstName、lastName和checkbox，然后我们将这些数据设置到本地存储(Local Storage)。你可以使用本地存储(Local Storage)来在浏览器中的存储数据。</p>
<p>现在，在更新组件中，我们需要一个表单来进行更新操作。让我们复制(reate component)创建组件中的表单。只要把函数的名称从Create改为Update。</p>
<pre><code>import React, { useState } from 'react';
import { Button, Checkbox, Form } from 'semantic-ui-react'
import axios from 'axios';

export default function Update() {
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [checkbox, setCheckbox] = useState(false);

    return (
        &lt;div&gt;
            &lt;Form className="create-form"&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;First Name&lt;/label&gt;
                    &lt;input placeholder='First Name' onChange={(e) =&gt; setFirstName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;Last Name&lt;/label&gt;
                    &lt;input placeholder='Last Name' onChange={(e) =&gt; setLastName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;Checkbox label='I agree to the Terms and Conditions' onChange={(e) =&gt; setCheckbox(!checkbox)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Button type='submit'&gt;Update&lt;/Button&gt;
            &lt;/Form&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>我们的更新页面</p>
<p>在Update组件中创建一个`useEffect'钩子(hook)。我们将用它来获取我们之前存储在本地存储的数据。同时，为ID字段再创建一个状态(state)。</p>
<pre><code>const [id, setID] = useState(null);

useEffect(() =&gt; {
        setID(localStorage.getItem('ID'))
        setFirstName(localStorage.getItem('First Name'));
        setLastName(localStorage.getItem('Last Name'));
        setCheckbox(localStorage.getItem('Checkbox Value'))
}, []);
</code></pre>
<p>根据你的keys(字典，map 数据结构)从本地存储设置相应的数据。我们需要在表格字段中设置这些值。当更新页面加载时，它将自动填入这些字段。</p>
<pre><code>&lt;Form className="create-form"&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;First Name&lt;/label&gt;
                    &lt;input placeholder='First Name' value={firstName} onChange={(e) =&gt; setFirstName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;Last Name&lt;/label&gt;
                    &lt;input placeholder='Last Name' value={lastName} onChange={(e) =&gt; setLastName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;Checkbox label='I agree to the Terms and Conditions' checked={checkbox} onChange={(e) =&gt; setCheckbox(!checkbox)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Button type='submit'&gt;Update&lt;/Button&gt;
            &lt;/Form&gt;
</code></pre>
<p>设置表格字段的值</p>
<p>现在，如果我们点击阅读页面中的更新按钮，我们将被重定向到更新页面，在那里我们将看到所有根据数据自动填充的表单。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-193521.png" alt="更新页面" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>更新页面</figcaption>
</figure>
<p>现在，让我们创建更新请求来更新数据。</p>
<p>创建一个名为<code>updateAPIData</code>的函数。在这个函数中，我们将使用axios.put来发送一个PUT请求，以更新我们的数据。</p>
<pre><code>const updateAPIData = () =&gt; {
    axios.put(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData/${id}`, {
        firstName,
         lastName,
         checkbox
	})
}
</code></pre>
<p>在这里，你可以看到我们在API端点上附加了一个id字段。</p>
<p>当我们点击表中的字段时，它的ID会被存储到本地存储器(Local Storage)中。而在更新页面，我们正在检索它。然后，我们将该ID存储在_<code>id</code>_状态中。</p>
<p>之后，我们将ID传递给端点。这使我们能够更新我们传递ID的字段。</p>
<p>将<code>updateAPIData</code>函数绑定到更新按钮上。</p>
<pre><code>&lt;Button type='submit' onClick={updateAPIData}&gt;Update&lt;/Button&gt;
</code></pre>
<p>将updateAPIData绑定到更新按钮上</p>
<p>点击读取页面中表格的更新按钮，改变你的姓氏（last name），然后点击更新页面中的更新按钮。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-194627.png" alt="更新字段" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>更新字段</figcaption>
</figure>
<p>回到“阅读”页面，或查看API。你会看到你的姓氏(last name)已被改变。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-194756.png" alt="The Mock API" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>The Mock API</figcaption>
</figure>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-194822.png" alt="我们的读取表格" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>我们的读取表格</figcaption>
</figure>
<h3 id="">删除操作</h3>
<p>在读取表(read table)中再添加一个Button，我们将用它来进行删除操作。</p>
<pre><code>&lt;Table.Cell&gt;
   &lt;Button onClick={() =&gt; onDelete(data.id)}&gt;Delete&lt;/Button&gt;
&lt;/Table.Cell&gt;
</code></pre>
<p>读取表中的删除按钮</p>
<p>创建一个名为 "onDelete "的函数，并将此函数绑定到删除按钮上。这个函数将在点击删除按钮时接收一个ID参数。</p>
<pre><code>const onDelete = (id) =&gt; {

}
</code></pre>
<p>The Delete Function</p>
<p>We are going to use axios.delete to delete the respective columns.</p>
<pre><code>const onDelete = (id) =&gt; {
  axios.delete(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData/${id}`)
}
</code></pre>
<p>从API中删除字段</p>
<p>点击删除按钮并检查API。你会看到数据已经被删除。</p>
<p>我们需要在表被删除后获得该表的数据。</p>
<p>因此，创建一个函数来获得API数据。</p>
<pre><code>const getData = () =&gt; {
    axios.get(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData`)
        .then((getData) =&gt; {
             setAPIData(getData.data);
         })
}
</code></pre>
<p>获取API数据</p>
<p>现在，在<code>onDelete</code>函数中，我们需要在删除一个字段后获得更新的数据。</p>
<pre><code>const onDelete = (id) =&gt; {
        axios.delete(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData/${id}`)
     .then(() =&gt; {
        getData();
    })
}
</code></pre>
<p>删除一个字段后获得更新数据</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-201047.png" alt="读取表格" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>读取表格</figcaption>
</figure>
<p>因此，现在如果我们在任何字段上点击Delete，它将删除该字段并自动刷新表格。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-201423.png" alt="删除一个字段后读表" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>删除一个字段后读表</figcaption>
</figure>
<h2 id="crud">让我们对我们的CRUD应用程序做一些改进吧</h2>
<p>因此，当我们在Create页面发布我们的数据时，我们只是在模拟(mock)数据库中获得数据。当我们的数据在创建页面中被创建时，我们需要重定向到读取页面。</p>
<p>从React Router导入`useHistory'。</p>
<pre><code>import { useHistory } from 'react-router';
</code></pre>
<p>从React Router导入useHistory</p>
<p>创建变量<code>history</code>使用 <code>let</code>。</p>
<pre><code>let history = useHistory();
</code></pre>
<p>然后，使用history.push函数，在post API被调用后推送到阅读页面。</p>
<pre><code>const postData = () =&gt; {
        axios.post(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData`, {
            firstName,
            lastName,
            checkbox
        }).then(() =&gt; {
            history.push('/read')
        })
    }
</code></pre>
<p>在发布API成功后推送到阅读页面</p>
<p>它将使用<code>useHistory</code>钩子(hook)推送到阅读页面。</p>
<p>对更新页面做同样的处理。</p>
<pre><code>import React, { useState, useEffect } from 'react';
import { Button, Checkbox, Form } from 'semantic-ui-react'
import axios from 'axios';
import { useHistory } from 'react-router';

export default function Update() {
    let history = useHistory();
    const [id, setID] = useState(null);
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [checkbox, setCheckbox] = useState(false);

    useEffect(() =&gt; {
        setID(localStorage.getItem('ID'))
        setFirstName(localStorage.getItem('First Name'));
        setLastName(localStorage.getItem('Last Name'));
        setCheckbox(localStorage.getItem('Checkbox Value'));
    }, []);

    const updateAPIData = () =&gt; {
        axios.put(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData/${id}`, {
            firstName,
            lastName,
            checkbox
        }).then(() =&gt; {
            history.push('/read')
        })
    }
    return (
        &lt;div&gt;
            &lt;Form className="create-form"&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;First Name&lt;/label&gt;
                    &lt;input placeholder='First Name' value={firstName} onChange={(e) =&gt; setFirstName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;Last Name&lt;/label&gt;
                    &lt;input placeholder='Last Name' value={lastName} onChange={(e) =&gt; setLastName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;Checkbox label='I agree to the Terms and Conditions' checked={checkbox} onChange={() =&gt; setCheckbox(!checkbox)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Button type='submit' onClick={updateAPIData}&gt;Update&lt;/Button&gt;
            &lt;/Form&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Update.js</p>
<p>现在你知道如何使用React和React Hooks进行CRUD操作了吧！</p>
<p>另外，如果你想补充学习，你可以观看我在Youtube上的[React CRUD操作视频]（<a href="https://youtu.be/-ZMP8ZladIQ%EF%BC%89%E3%80%82">https://youtu.be/-ZMP8ZladIQ）。</a></p>
<blockquote>
<p><strong>学习愉快</strong></p>
</blockquote>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 通过创建贷款计算器学习 React ]]>
                </title>
                <description>
                    <![CDATA[ 今天我们将通过创建一个贷款计算器来学习和实践 ReactJS。这就是我们要创建的项目 👇  * 这是项目的在线 demo [https://mortgage-calculator-tutorial.vercel.app/]  * 这是 GitHub 仓库链接 [https://github.com/JoyShaheb/mortgage-calculator-tutorial] 目标 在创建这个项目时，我们将涉及的主题是：  * React 功能组件  * Material UI  * 用户输入  * 处理 Props  * Props 解构赋值  * useState Hook 还有更多！这个教程对于想通过创建一个真实世界的项目来学习 ReactJS 的初学者来说是非常好的。 如果你喜欢，你也可以在 YouTube 上观看这个教程： 通过创建一个贷款计算器来学习 React目录  * 项目设置 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/react-mortgage-calculator-tutorial-for-beginners/</link>
                <guid isPermaLink="false">64255e3ae32a7606487d5d80</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chengjun.L ]]>
                </dc:creator>
                <pubDate>Tue, 04 Apr 2023 03:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/04/thumbnail_EN--2-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/react-mortgage-calculator-tutorial-for-beginners/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Learn React by Building a Mortgage Calculator</a>
      </p><p>今天我们将通过创建一个贷款计算器来学习和实践 ReactJS。这就是我们要创建的项目 👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ryoc8jihbyprgp50ulhm.png" class="kg-image" alt="Project Image" width="600" height="400" loading="lazy"></figure><ul><li><a href="https://mortgage-calculator-tutorial.vercel.app/">这是项目的在线 demo</a></li><li><a href="https://github.com/JoyShaheb/mortgage-calculator-tutorial">这是 GitHub 仓库链接</a></li></ul><h2 id="-"><strong>目标</strong></h2><p>在创建这个项目时，我们将涉及的主题是：</p><ul><li>React 功能组件</li><li>Material UI</li><li>用户输入</li><li>处理 Props</li><li>Props 解构赋值</li><li>useState Hook</li></ul><p>还有更多！这个教程对于想通过创建一个真实世界的项目来学习 ReactJS 的初学者来说是非常好的。</p><h2 id="-youtube-">如果你喜欢，你也可以在 YouTube 上观看这个教程：</h2><figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.17944444444445%;" class="fluid-width-video-wrapper">
            <iframe src="https://www.youtube.com/embed/uluphP4xXD8?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="Embedded content" loading="lazy" name="fitvid0" style="box-sizing: inherit; margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-optical-sizing: inherit; font-kerning: inherit; font-feature-settings: inherit; font-variation-settings: inherit; font-size: 22px; vertical-align: middle; position: absolute; top: 0px; left: 0px; width: 720px; height: 404.492px;"></iframe>
          </div>
        </div>
      </figure><h2 id="--1"><strong>目录</strong></h2><ul><li>项目设置</li><li>文件夹结构</li><li>Material UI 主题</li><li>如何创建导航条</li><li>Material UI 网格系统</li><li>如何创建 Slider 组件</li><li>休息一下</li><li>如何使用 useState Hook</li><li>如何创建 SliderSelect 组件</li><li>如何创建 TenureSelect 组件</li><li>如何创建 Result 组件</li><li>总结</li><li>我的社交媒体链接</li></ul><h2 id="--2"><strong>项目设置</strong></h2><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/02wjpjpo4k6tjg78ynu4.png" class="kg-image" alt="Project Setup" width="600" height="400" loading="lazy"></figure><p>为了设置该项目，我们需要安装 <code>eact</code>、<code>material-ui</code> 和其他必要的软件包。</p><p>首先创建一个名为 <code>mortgage-calculator</code> 的文件夹，在 VS Code 上打开它，然后在终端运行以下命令：</p><pre><code class="language-bash">npx create-react-app .
npm install @mui/material @emotion/react @emotion/styled
npm install --save chart.js react-chartjs-2
</code></pre><h3 id="app-js"><strong>App.js</strong></h3><p>我们将删除 <code>app.js</code> 中所有的模板代码，保留这部分 👇</p><pre><code class="language-js">import React from "react";

function App() {
  return &lt;div className="App"&gt;Hello everyone&lt;/div&gt;;
}

export default App;
</code></pre><p>然后在终端运行这个命令来启动服务器：</p><pre><code class="language-bash">npm start
</code></pre><p>该项目现在在 web 浏览器上应该是完全空白的。</p><h3 id="--3"><strong>开始编程</strong></h3><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qew6q8zwn723s86md4rf.png" class="kg-image" alt="Lets start coding" width="600" height="400" loading="lazy"></figure><p>一切都设置好了，可以开始了。现在，我们将开始构建该项目 :)</p><h2 id="--4"><strong>文件夹结构</strong></h2><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7hujbmnjhri7470r2rxk.png" class="kg-image" alt="Folder Structure" width="600" height="400" loading="lazy"></figure><p>我们的文件夹结构应该是这样的，这样我们就可以轻松地管理和维护文件和文件夹：</p><pre><code class="language-bash">mortgage-calculator/
├── src/
│   ├── Components/
│   │   ├── Common/
│   │   │   └── SliderComponent.js
│   │   ├── Navbar.js
│   │   ├── Result.js
│   │   ├── SliderSelect.js
│   │   ├── TenureSelect.js
│   ├── theme.js
│   ├── App.js
│   ├── index.js
├── .gitignore
├── package.json
└── package-lock.json
</code></pre><p>如果你感到困惑，这里有一张我们的项目文件夹结构的图片：</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9q9ezw36rp0mcfo12qsw.png" class="kg-image" alt="Folder Structure" width="600" height="400" loading="lazy"></figure><h2 id="material-ui-"><strong>Material UI 主题</strong></h2><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mxoseeckq7zgnidn4yol.png" class="kg-image" alt="MUI Theme" width="600" height="400" loading="lazy"></figure><p>我们将使用 Material UI 的深色主题。为此，我们需要在 <code>src</code> 文件夹中创建一个名为 <code>theme.js</code> 的文件，并添加以下代码：</p><h3 id="theme-js"><strong>theme.js</strong></h3><pre><code class="language-js">import { createTheme } from '@mui/material/styles';

export const theme = createTheme({
  palette: {
    mode: 'dark',
  },
})
</code></pre><h3 id="index-js"><strong>index.js</strong></h3><p>接下来，我们需要在 <code>index.js</code> 文件中导入 <code>theme</code>，并用 <code>ThemeProvider</code> 来包含应用程序。下面就跟着做吧：👇</p><pre><code class="language-js">import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import { theme } from "./theme";

&lt;React.StrictMode&gt;
  &lt;ThemeProvider theme={theme}&gt;
    &lt;App /&gt;
    &lt;CssBaseline /&gt;
  &lt;/ThemeProvider&gt;
&lt;/React.StrictMode&gt;
</code></pre><p><strong><strong>注意</strong>：</strong>如果你不传递 <code>CssBaseline</code> 组件，我们将无法看到 MUI 的深色主题。</p><p>这是目前的结果： 👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j117t2x35qebkx3qf21r.png" class="kg-image" alt="Result so far" width="600" height="400" loading="lazy"></figure><p>整个屏幕将是黑的。这意味着我们的项目已经应用了 Material UI 深色模式。</p><h2 id="-navbar"><strong>如何创建 Navbar</strong></h2><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tx7urj6lh5710anwtqa8.png" class="kg-image" alt="Navbar Setup" width="600" height="400" loading="lazy"></figure><p>接下来，我们将创建一个非常简单的导航条来显示 logo。为此，我们需要在 <code>src/Components</code> 文件夹中创建一个名为 <code>Navbar.js</code> 的文件，并添加以下代码：</p><h3 id="navbar-js"><strong>Navbar.js</strong></h3><pre><code class="language-js">import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import { Container } from "@mui/system";

const Navbar = () =&gt; {
  return (
    &lt;AppBar position="static"&gt;
      &lt;Container maxWidth='xl'&gt;
        &lt;Toolbar&gt;
          &lt;Typography variant="h5"&gt;
            Bank of React
          &lt;/Typography&gt;
        &lt;/Toolbar&gt;
      &lt;/Container&gt;
    &lt;/AppBar&gt;
  );
};

export default Navbar;
</code></pre><p>下面是对 Material UI 中使用的组件的简单解释：</p><ul><li><strong><strong>AppBar</strong>：</strong>Material UI 的 AppBar 组件用于在用户界面上创建一个顶部导航栏。点击<a href="https://mui.com/material-ui/react-app-bar/">这里</a>了解更多。</li><li><strong><strong>Container</strong>：</strong>Material UI 的 Container 组件用于创建一个容器元素，该元素可用于创建一个响应式布局，并在用户界面中集中和包含其他元素。点击<a href="https://mui.com/material-ui/react-container/">这里</a>了解更多。</li><li><strong><strong>ToolBar</strong>：</strong>Toolbar 组件可以包含诸如按钮、文本和图标等元素，也可以用来创建一个适应不同屏幕尺寸的响应式布局。点击<a href="https://mui.com/material-ui/api/toolbar/">这里</a>了解更多。</li><li><strong><strong>Typography</strong>：</strong>Material UI 的 typography 组件用于将预定义的排版样式应用于文本元素。它可以帮助创建一个一致的、视觉上赏心悦目的布局，具有可定制的字体、大小、粗细和间距。点击<a href="https://mui.com/material-ui/react-typography/">这里</a>了解更多。</li></ul><h3 id="app-js-1"><strong>App.js</strong></h3><p>最后，将其导入 <code>App.js</code>，并这样写代码：👇</p><pre><code class="language-js">import React from "react";
import Navbar from "./Components/Navbar";

function App() {
  return (
    &lt;div className="App"&gt;
      &lt;Navbar /&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre><p>这是目前为止的结果： 👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lv52resgdtg2wpgqq4xa.png" class="kg-image" alt="Navbar result" width="600" height="400" loading="lazy"></figure><h2 id="material-ui--1"><strong>Material UI 网格系统</strong></h2><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bd60xyrgs28g75eulshc.png" class="kg-image" alt="MUI Grid System" width="600" height="400" loading="lazy"></figure><p>在最终完成的项目中，我们可以看到内容被分成了两部分。左边有滑块组件，右边有饼图。这是用 <a href="https://mui.com/material-ui/react-grid/">Material UI 的网格系统</a>实现的。</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ezdk2jrt1dg88wac6iyu.png" class="kg-image" alt="Finalized project image" width="600" height="400" loading="lazy"></figure><p>不仅如此，我们还可以看到，内容在较小的屏幕尺寸上是响应式的。这也是通过使用 Material UI 网格系统实现的。</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5a1e5356nbnr74y6r6ze.png" class="kg-image" alt="Responsive content" width="600" height="400" loading="lazy"></figure><p>为了复制这一点，我们需要在 App.js 文件上写上这些东西。你可以在这里跟着做。👇</p><p>首先，我们需要从 Material UI 和组件文件夹中导入所有需要的组件：</p><pre><code class="language-js">import React, { useState } from "react";
import { Grid } from "@mui/material";
import { Container } from "@mui/system";
import Navbar from "./Components/Navbar";
import Result from "./Components/Result";
import SliderSelect from "./Components/SliderSelect";
import TenureSelect from "./Components/TenureSelect";
</code></pre><p>接下来，我们在 <code>return</code> 语句里面写这段代码：👇</p><pre><code class="language-js">&lt;div className="App"&gt;
  &lt;Navbar /&gt;
  &lt;Container maxWidth="xl" sx={{marginTop:4}}&gt;
    &lt;Grid container spacing={5} alignItems="center"&gt;
      &lt;Grid item xs={12} md={6}&gt;
        &lt;SliderSelect /&gt;
        &lt;TenureSelect /&gt;
      &lt;/Grid&gt;
      &lt;Grid item xs={12} md={6}&gt;
        &lt;Result/&gt;
      &lt;/Grid&gt;
    &lt;/Grid&gt;
  &lt;/Container&gt;
&lt;/div&gt;
</code></pre><p>对这段代码的解释：</p><ul><li><strong><strong>Container</strong>：</strong>在 <code>Container</code> 上，我们写了 <code>sx={{marginTop:4}}</code>。这是在 Material UI 中为组件添加内联样式的方法。</li><li><strong>Grid：</strong>Grid 组件被用来创建一个适应不同屏幕尺寸的响应式布局。<code>Grid container</code> 代表父元素，<code>Grid item</code> 代表子元素。</li><li>在 <code>Grid</code> 组件上，我们写了 <code>spacing={5}</code>。这是在网格项之间添加间距的方法。</li><li>在 <code>Grid</code> 组件上，我们还写了 <code>xs={12}</code>，这意味着在超小屏幕上，网格项将占据整个屏幕的宽度。同样地，<code>md={6}</code> 意味着在中等和较大的屏幕上，网格项将占到屏幕的一半。这就是我们如何使组件具有响应性。</li></ul><p>这是目前为止的结果：👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jr80ud2oawv7nj6xti75.png" class="kg-image" alt="Result image of Grid system" width="600" height="400" loading="lazy"></figure><h2 id="-slider-"><strong>如何创建 Slider 组件</strong></h2><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1psq4l7lbwl1c2aizo6j.png" class="kg-image" alt="Slider Component" width="600" height="400" loading="lazy"></figure><p>接下来，我们将创建一个滑块组件来获取用户的输入金额。它看起来将是这样的： 👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oth4rtfgebeylr1kjktn.png" class="kg-image" alt="Slider Component" width="600" height="400" loading="lazy"></figure><p>为此，我们需要在 <code>src/Components/Common</code> 文件夹中创建一个名为 <code>SliderComponent.js</code> 的文件。首先，让我们列出所有需要传递给可重用的滑块组件的 props：</p><ul><li><strong><strong>label</strong></strong></li><li><strong><strong>min</strong></strong></li><li><strong><strong>max</strong></strong></li><li><strong><strong>defaultValue</strong></strong></li><li><strong><strong>unit</strong></strong></li><li><strong><strong>value</strong></strong></li><li><strong><strong>steps</strong></strong></li><li><strong><strong>amount</strong></strong></li><li><strong><strong>onChange</strong></strong></li></ul><h3 id="slidercomponent-js"><strong>SliderComponent.js</strong></h3><p>我们开始吧。首先，在 <code>SliderComponent.js</code> 文件中从 MUI 导入以下组件：</p><pre><code class="language-js">import React from "react";
import Slider from "@mui/material/Slider";
import { Typography } from "@mui/material";
import { Stack } from "@mui/system";
</code></pre><p>我们将使用 <a href="https://mui.com/material-ui/react-stack/">MUI 的 Stack 组件</a>来垂直堆叠组件。<code>my</code> 是 <code>marginY</code> [margin-top &amp; margin-bottom] 的缩写。我们将使用 MUI 的 <code>Typography</code> 组件来显示标签、单位和其他数据。我们将使用 MUI 的 <code>Slider</code> 组件来显示滑块。</p><p>先写这一小段代码，解构 props：</p><pre><code class="language-js">const SliderComponent = ({
  defaultValue,
  min,
  max,
  label,
  unit,
  onChange,
  amount,
  value,
  steps
}) =&gt; {
  return (
    &lt;Stack my={1.4}&gt;

    &lt;/Stack&gt;
  )
}

export default SliderComponent
</code></pre><p>我们将编写这段代码来显示标签、单位和金额。</p><pre><code class="language-jsx">&lt;Stack gap={1}&gt;
  &lt;Typography variant="subtitle2"&gt;{label}&lt;/Typography&gt;
  &lt;Typography variant="h5"&gt;
    {unit} {amount}
  &lt;/Typography&gt;
&lt;/Stack&gt;
</code></pre><p>编写这段代码来显示滑块，并像这样把 props 传递给滑块组件： 👇</p><pre><code class="language-jsx">&lt;Slider
  min={min}
  max={max}
  defaultValue={defaultValue}
  aria-label="Default"
  valueLabelDisplay="auto"
  onChange={onChange}
  value={value}
  marks
  step={steps}
/&gt;
</code></pre><p>我们将编写这段代码来显示滑块的最小和最大值。我们将使用 MUI 的 <code>Stack</code> 组件来水平堆叠组件。<code>direction="row"</code> 是 <code>flex-direction: row</code> 的缩写。<code>justifyContent="space-between"</code> 是 <code>justify-content: space-between</code> 的缩写。</p><pre><code class="language-js">&lt;Stack direction="row" justifyContent="space-between"&gt;
  &lt;Typography variant="caption" color="text.secondary"&gt;
    {unit} {min}
  &lt;/Typography&gt;
  &lt;Typography variant="caption" color="text.secondary"&gt;
    {unit} {max}
  &lt;/Typography&gt;
&lt;/Stack&gt;
</code></pre><p>到目前为止，干得不错！</p><h2 id="--5"><strong>休息一下</strong></h2><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k9s9yorz1gwt380tbr7t.png" class="kg-image" alt="Take a break" width="600" height="400" loading="lazy"></figure><p>休息一会儿吧——你值得！🎉</p><h2 id="-usestate-hook"><strong>如何使用 useState Hook</strong></h2><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/89cx3gxzdl5h7q0okf8f.png" class="kg-image" alt="useState Hook" width="600" height="400" loading="lazy"></figure><p>我们需要在我们的项目中使用 useState hook。但在此之前，我们需要了解它是什么以及为什么我们需要使用它。</p><p>useState hook 是一个内置的 React 函数，允许你向功能组件添加状态。它返回一个包含两个元素的<code>数组</code>：当前状态值和一个更新该值的函数。useState hook 的一般语法如下：</p><pre><code class="language-js">const [state, setState] = useState(initialState);
</code></pre><p>其中👇</p><ul><li><code>state</code>：将存储状态的常量或变量的名称</li><li><code>setState</code>：一个更新状态的函数</li><li><code>initialState</code>：状态的初始值</li></ul><h3 id="usestate-hook-">useState hook 的例子</h3><p>我们将创建一个切换按钮，点击它时，它的文本在 “ON” 和 “OFF” 之间切换。</p><pre><code class="language-js">import React, { useState } from 'react';

const ToggleButton = () =&gt; {
  const [isOn, setIsOn] = useState(false);

  const toggle = () =&gt; setIsOn(!isOn)

  return (
      &lt;button onClick={toggle}&gt;{isOn ? 'ON' : 'OFF'}&lt;/button&gt;
  );
};

export default ToggleButton;

</code></pre><p>在这里，我们初始化 <code>isOn</code> 状态的初始值为 <code>false</code>。当用户点击按钮时，<code>toggle</code> 函数将 <code>isOn</code> 状态更新为其相反的值。我们使用一个<code>三元操作符</code>，根据 <code>isOn</code> 的当前值来渲染按钮内的文本。</p><h3 id="app-js-2"><strong>App.js</strong></h3><p>现在让我们回到我们的项目中来。首先，在 <code>App.js</code> 文件中，从 React 导入 <code>useState</code> hook。</p><pre><code class="language-js">import React, { useState } from 'react';
</code></pre><p>接下来，我们将使用 <code>useState</code> hook 声明一个状态来存储滑块的值。我们将在 <code>useState</code> hook 中以 <code>{}</code> 的形式传递状态的初始值，将数据存储为一个对象。</p><pre><code class="language-js">function App() {
  const [data, setData] = useState({})

  // other codes are here
}
</code></pre><p>我们使用 &nbsp;useState hook 来创建一个名为 <code>data</code> 的新状态变量和一个名为 <code>setData</code> 的函数，我们可以用它来更新这个状态。</p><p>接下来，我们将把这些值作为默认值传递给滑块组件。</p><pre><code class="language-js">function App() {
  const [data, setData] = useState({
    homeValue: 3000,
    downPayment: 3000 * 0.2,
    loanAmount: 3000 * 0.8,
    loanTerm: 5,
    interestRate: 5,
  })

  // other codes are here
}
</code></pre><p>然后，我们将把 <code>data</code> 和 <code>setData</code> 状态作为一个 prop 传递给 <code>SliderSelect</code> 组件，像这样：👇</p><pre><code class="language-js">&lt;div className="App"&gt;
  &lt;Navbar /&gt;
  &lt;Container maxWidth="xl" sx={{marginTop:4}}&gt;
    &lt;Grid container spacing={5} alignItems="center"&gt;
      &lt;Grid item xs={12} md={6}&gt;

        {/* this is where we write the code  👇 */}
        &lt;SliderSelect data={data} setData={setData}/&gt;

        &lt;TenureSelect /&gt;
      &lt;/Grid&gt;
      &lt;Grid item xs={12} md={6}&gt;
        &lt;Result/&gt;
      &lt;/Grid&gt;
    &lt;/Grid&gt;
  &lt;/Container&gt;
&lt;/div&gt;
</code></pre><h2 id="-sliderselect-"><strong>如何创建 SliderSelect 组件</strong></h2><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/001ovrcapajjl5i480dn.png" class="kg-image" alt="SliderSelect.js component" width="600" height="400" loading="lazy"></figure><p>所以现在我们已经准备好了可重复使用的 <code>SliderComponent</code>，将在 <code>SliderSelect.js</code> 组件中使用它。首先，从 <code>Common</code> 文件夹中导入 <code>SliderComponent</code> 组件。</p><h3 id="sliderselect-js"><strong>SliderSelect.js</strong></h3><pre><code class="language-js">import SliderComponent from "./Common/SliderComponent";
</code></pre><p>接下来，我们将对从 <code>App.js</code> 收到的 prop 进行解构。同时，创建一个名为 <code>bank_limit</code> 的变量，并给它一个 <code>10000</code> 的值。这代表了一个人可以从我们的银行借到的最大数额的钱。</p><pre><code class="language-js">import React from "react";
import SliderComponent from "./Common/SliderComponent";

const SliderSelect = ({ data, setData }) =&gt; {
  const bank_limit = 10000;
  return (
    &lt;div&gt;
      
    &lt;/div&gt;
  );
};

export default SliderSelect;

</code></pre><p>接下来，我们将使用 <code>SliderComponent</code> 来显示名为 <code>Home Value</code> 的滑块。在这里，我们将像这样把 props 传递给 <code>SliderComponent</code> 组件。</p><pre><code class="language-js">const SliderSelect = ({ data, setData }) =&gt; {
  const bank_limit = 10000;
  return (
    &lt;div&gt;
      &lt;SliderComponent
        onChange={(e, value) =&gt; {
          setData({
            ...data,
            homeValue: value.toFixed(0),
            downPayment: (0.2 * value).toFixed(0),
            loanAmount: (0.8 * value).toFixed(0),
          });
        }}
        defaultValue={data.homeValue}
        min={1000}
        max={bank_limit}
        steps={100}
        unit="$"
        amount={data.homeValue}
        label="Home Value"
        value={data.homeValue}
      /&gt;
    &lt;/div&gt;
  );
};

</code></pre><p>这是目前为止的结果：👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tc8ymi79urkugw7kd4ci.png" class="kg-image" alt="Home Value Slider" width="600" height="400" loading="lazy"></figure><p>我们将以同样的方式为 <code>Down Payment</code> 和 <code>Loan Amount</code> 创建滑块，像这样：👇</p><pre><code class="language-js">  return (
    &lt;div&gt;
      {/* other codes are here */}

      &lt;SliderComponent
        onChange={(e, value) =&gt;
          setData({
            ...data,
            downPayment: value.toFixed(0),
            loanAmount: (data.homeValue - value).toFixed(0),
          })
        }
        defaultValue={data.downPayment}
        min={0}
        max={data.homeValue}
        steps={100}
        unit="$"
        amount={data.downPayment}
        label="Down Payment"
        value={data.downPayment}
      /&gt;

      &lt;SliderComponent
        onChange={(e, value) =&gt;
          setData({
            ...data,
            loanAmount: value.toFixed(0),
            downPayment: (data.homeValue - value).toFixed(0),
          })
        }
        defaultValue={data.loanAmount}
        min={0}
        max={data.homeValue}
        steps={100}
        unit="$"
        amount={data.loanAmount}
        label="Loan Amount"
        value={data.loanAmount}
      /&gt;
    &lt;/div&gt;
  );
</code></pre><p>这是目前为止的结果：👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nvhfgzpf1aq02p3kwdqz.png" class="kg-image" alt="the result so far" width="600" height="400" loading="lazy"></figure><p>最后，我们将为 <code>Interest Rate</code> 创建滑块。你可以在这里跟着做：👇</p><pre><code class="language-js">return (
    &lt;div&gt;
      {/* other codes are here */}

      &lt;SliderComponent
        onChange={(e, value) =&gt;
          setData({
            ...data,
            interestRate: value,
          })
        }
        defaultValue={data.interestRate}
        min={2}
        max={18}
        steps={0.5}
        unit="%"
        amount={data.interestRate}
        label="Interest Rate"
        value={data.interestRate}
      /&gt;
    &lt;/div&gt;
  );
</code></pre><p>结果如下：👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/snlpyvu2qfqzt81ecbvo.png" class="kg-image" alt="Interest Rate slider" width="600" height="400" loading="lazy"></figure><h2 id="-tenureselect-"><strong>如何创建 TenureSelect 组件</strong></h2><p>接下来，我们将创建 <code>TenureSelect</code> 组件。这个组件将被用来选择贷款的期限。它看起来像这样：👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/70arqood9dwqj9j46apk.png" class="kg-image" alt="Image description" width="600" height="400" loading="lazy"></figure><h3 id="app-js-3"><strong>App.js</strong></h3><p>首先，像这样把 <code>data</code> 和 <code>setData</code> 状态作为一个 prop 传递给 <code>TenureSelect</code> 组件：👇</p><pre><code class="language-js">return (
  &lt;div className="App"&gt;
    &lt;Navbar /&gt;
    &lt;Container maxWidth="xl" sx={{marginTop:4}}&gt;
      &lt;Grid container spacing={5} alignItems="center"&gt;
        &lt;Grid item xs={12} md={6}&gt;
          &lt;SliderSelect data={data} setData={setData} /&gt;

          {/* this is where we write the code  👇 */}
          &lt;TenureSelect data={data} setData={setData}/&gt;

        &lt;/Grid&gt;
        &lt;Grid item xs={12} md={6}&gt;
          &lt;Result data={data}/&gt;
        &lt;/Grid&gt;
      &lt;/Grid&gt;
    &lt;/Container&gt;
  &lt;/div&gt;
);
</code></pre><h3 id="tenureselect-js"><strong>TenureSelect.js</strong></h3><p>然后，从 <code>MUI</code> 库中导入这些所需的组件：</p><pre><code class="language-js">import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
</code></pre><p>然后解构我们从 <code>App.js</code> 接收的 props，同时创建一个名为 <code>handleChange</code> 的函数，它将被用来设置 <code>tenure</code> 状态，像这样：👇</p><pre><code class="language-js">const TenureSelect = ({ data, setData }) =&gt; {

  const handleChange = (event) =&gt; {
    setData({...data, loanTerm: event.target.value});
  };

  return ()
};

export default TenureSelect;
</code></pre><p>接下来，我们将创建 <code>TenureSelect</code> 组件。它看起来像这样：👇</p><pre><code class="language-js">return (
  &lt;FormControl fullWidth&gt;
    &lt;InputLabel id="demo-simple-select-label"&gt;Tenure&lt;/InputLabel&gt;
    &lt;Select
      labelId="demo-simple-select-label"
      id="demo-simple-select"
      value={data.loanTerm}
      label="Tenure"
      defaultValue={5}
      onChange={handleChange}
    &gt;
      &lt;MenuItem value={5}&gt;5 years&lt;/MenuItem&gt;
      &lt;MenuItem value={10}&gt;10 years&lt;/MenuItem&gt;
      &lt;MenuItem value={15}&gt;15 years&lt;/MenuItem&gt;
      &lt;MenuItem value={20}&gt;20 years&lt;/MenuItem&gt;
      &lt;MenuItem value={25}&gt;25 years&lt;/MenuItem&gt;
    &lt;/Select&gt;
  &lt;/FormControl&gt;
);
</code></pre><p>结果如下：👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fl0fsfk2lv9dnh588eyh.png" class="kg-image" alt="The result so far" width="600" height="400" loading="lazy"></figure><h2 id="-result-"><strong>如何创建 Result 组件</strong></h2><p>最后，我们将创建 <code>Result</code> 组件。这个组件将用于显示每月的贷款分期付款和饼图。它看起来像这样：👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7f5vgfcsk6aj6yseqvi1.png" class="kg-image" alt="Result component" width="600" height="400" loading="lazy"></figure><h3 id="app-js-4"><strong>App.js</strong></h3><p>首先，像这样把 <code>data</code> 状态作为一个 prop 传递给 <code>Result</code> 组件：👇</p><pre><code class="language-js">return (
  &lt;div className="App"&gt;
    &lt;Navbar /&gt;
    &lt;Container maxWidth="xl" sx={{marginTop:4}}&gt;
      &lt;Grid container spacing={5} alignItems="center"&gt;
        &lt;Grid item xs={12} md={6}&gt;
          &lt;SliderSelect data={data} setData={setData} /&gt;
          &lt;TenureSelect data={data} setData={setData}/&gt;
        &lt;/Grid&gt;
        &lt;Grid item xs={12} md={6}&gt;

          {/* this is where we write the code  👇 */}
          &lt;Result data={data}/&gt;
          
        &lt;/Grid&gt;
      &lt;/Grid&gt;
    &lt;/Container&gt;
  &lt;/div&gt;
);
</code></pre><h3 id="result-js"><strong>Result.js</strong></h3><p>接下来，像这样导入所需的组件：👇</p><pre><code class="language-js">import React from "react";
import { Stack, Typography } from "@mui/material";
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
import { Pie } from "react-chartjs-2";

ChartJS.register(ArcElement, Tooltip, Legend);
</code></pre><p>然后像这样解构我们从 <code>App.js`</code>接收的数据状态：👇</p><pre><code class="language-js">const Result = ({ data }) =&gt; {
  const { homeValue, loanAmount, loanTerm, interestRate } = data;
  return ();
};

export default Result;
</code></pre><p>接下来我们将写出所有帮助我们进行计算的东西：👇</p><pre><code class="language-js">  const totalLoanMonths = loanTerm * 12;
  const interestPerMonth = interestRate / 100 / 12;
  const monthlyPayment =
    (loanAmount *
      interestPerMonth *
      (1 + interestPerMonth) ** totalLoanMonths) /
    ((1 + interestPerMonth) ** totalLoanMonths - 1);

  const totalInterestGenerated = monthlyPayment * totalLoanMonths - loanAmount;
</code></pre><p>然后我们需要这个变量来存储饼图的所有数据，像这样：👇</p><pre><code class="language-js">const pieChartData = {
  labels: ["Principle", "Interest"],
  datasets: [
    {
      label: "Ratio of Principle and Interest",
      data: [homeValue, totalInterestGenerated],
      backgroundColor: ["rgba(255, 99, 132, 0.2)", "rgba(54, 162, 235, 0.2)"],
      borderColor: ["rgba(255, 99, 132, 1)", "rgba(54, 162, 235, 1)"],
      borderWidth: 1,
    },
  ],
};
</code></pre><p>最后，我们将创建 <code>Result</code> 组件，它是这样的：👇</p><pre><code class="language-js">return (
  &lt;Stack gap={3}&gt;
    &lt;Typography textAlign="center" variant="h5"&gt;
      Monthly Payment: $ {monthlyPayment.toFixed(2)}
    &lt;/Typography&gt;
    &lt;Stack direction="row" justifyContent="center"&gt;
      &lt;div&gt;
        &lt;Pie data={pieChartData} /&gt;
      &lt;/div&gt;
    &lt;/Stack&gt;
  &lt;/Stack&gt;
);
</code></pre><p>结果如下：👇</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gqs2st1o5fhlpoqnpqol.png" class="kg-image" alt="The result so far" width="600" height="400" loading="lazy"></figure><h2 id="--6"><strong>总结</strong></h2><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z7w7p11dm81ggzxd6a1t.png" class="kg-image" alt="Congratulations" width="600" height="400" loading="lazy"></figure><p>祝贺你读到最后！现在你可以自信地、有效地使用 React JS 和 Material UI 来创建很酷的项目。</p><p>你还学会了如何使用 React 的 useState hook，以及如何处理 props。我希望你喜欢这个教程。</p><h2 id="--7">导师计划</h2><p>如果你有兴趣学习更多关于 React JS 和 web 开发的知识，我正在进行一个导师计划。你可以在这里查看细节👉<a href="https://www.mentorlabs.academy/">Mentor Labs Academy</a>。</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v5oyscu7l16tr636ekqj.png" class="kg-image" alt="Mentorship Program" width="600" height="400" loading="lazy"></figure><h2 id="--8"><strong>我的社交媒体链接</strong></h2><ul><li><a href="https://www.linkedin.com/in/joyshaheb/">LinkedIn/ JoyShaheb</a></li><li><a href="https://www.youtube.com/c/joyshaheb">YouTube / JoyShaheb</a></li><li><a href="https://twitter.com/JoyShaheb">Twitter / JoyShaheb</a></li><li><a href="https://www.instagram.com/joyshaheb/">Instagram / JoyShaheb</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何为你的博客创建目录 ]]>
                </title>
                <description>
                    <![CDATA[ 当你访问文档网站时，你会注意到许多网站都有一个组件：<TableOfContent /> 组件。 它背后的想法是给读者一个关于他们试图获取的信息的“提示”。 这个功能反过来又帮助读者直接访问某个部分，这个部分为他们所面临的任何错误或问题提供了解决方案，因此他们无需阅读整篇文章。这有助于形成良好的用户体验，因为你最终为你的受众省去了额外的滚动和搜索的麻烦。 我有一个个人博客 [https://meje.dev/blog] ，我花了很多时间在上面。而在很长一段时间里，我想过要增加这个功能。它将帮助任何访问我的网站的人阅读起来更开心，并找到他们需要的东西。 这篇文章是对我的过程的总结，所以你不需要经历我所经历的问题。如果你想在你的博客上添加目录功能，你可以和我一起看看这个过程。 我分享了一段视频，说明该组件完成后的样子。你可以在这里 [https://twitter.com/calebolojo/status/1629113931066142720]查看视频。 如何从前页获取标题文本 为了建立一个目录功能，我知道我需要做什么。由于我的博客上的文章是用 markdown 写的，我只 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-build-a-table-of-contents-component/</link>
                <guid isPermaLink="false">6410418a7d792438faff0074</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chengjun.L ]]>
                </dc:creator>
                <pubDate>Mon, 13 Mar 2023 01:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/03/toc.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-build-a-table-of-contents-component/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Build a Table of Contents Component for Your Blog</a>
      </p><p>当你访问文档网站时，你会注意到许多网站都有一个组件：<code>&lt;TableOfContent /&gt;</code> 组件。</p><p>它背后的想法是给读者一个关于他们试图获取的信息的“提示”。</p><p>这个功能反过来又帮助读者直接访问某个部分，这个部分为他们所面临的任何错误或问题提供了解决方案，因此他们无需阅读整篇文章。这有助于形成良好的用户体验，因为你最终为你的受众省去了额外的滚动和搜索的麻烦。</p><p>我有一个个人<a href="https://meje.dev/blog">博客</a>，我花了很多时间在上面。而在很长一段时间里，我想过要增加这个功能。它将帮助任何访问我的网站的人阅读起来更开心，并找到他们需要的东西。</p><p>这篇文章是对我的过程的总结，所以你不需要经历我所经历的问题。如果你想在你的博客上添加目录功能，你可以和我一起看看这个过程。</p><p>我分享了一段视频，说明该组件完成后的样子。你可以在<a href="https://twitter.com/calebolojo/status/1629113931066142720">这里</a>查看视频。</p><h2 id="-">如何从前页获取标题文本</h2><p>为了建立一个目录功能，我知道我需要做什么。由于我的博客上的文章是用 markdown 写的，我只是使用 markdown 的超集——MDX——它允许我在 markdown 文件中使用 React 组件。</p><p>我的清单上的第一件事是获得一种在组件中渲染标题文本的方法。这样，当人们点击标题时，浏览器就会滚动到文章的那个点。</p><p>在 HTML 中，你可以通过使用锚标签并将其值传递给一个 <code>href</code> 属性来实现这一目的。</p><p>要让链接的文本指向一个部分，理想的方法是像下面的代码片段那样：</p><pre><code class="language-html">&lt;a href="#section-one"&gt;Go to section one&lt;/a&gt;
&lt;a href="#section-two"&gt;Go to section two&lt;/a&gt;
&lt;a href="#section-three"&gt;Go to section three&lt;/a&gt;

&lt;section id="section-one"&gt;some content&lt;/section&gt;
&lt;section id="section-two"&gt;yet, a content that seems weird&lt;/section&gt;
&lt;section id="section-three"&gt;some content, again&lt;/section&gt;
</code></pre><p>在上面的片段中，DOM 中的锚点标签通过 <code>id</code> 属性与各部分相关联。当你点击任何文本时，它会把你带到相应的部分。</p><p>有了这个心理模型，我想在我写过的所有文章中用标题来填充每篇文章的前页（frontmatter）。我知道这将有些压力，但我还是这么做了。</p><p>这就是 markdown 文件中的 frontmatter 的样子。前页包含了我博客上所有文章的元数据，诸如标题、发布日期、文章所属的标签或类别、描述、规范 URL，以及其他任何你想添加的东西，以提升文章的 SEO。</p><p>当你用 Next.js 和 MDX 构建博客时，这种模式很常见。它也有一个类似 YAML 的语法。</p><pre><code class="language-bash">---
id: 20
title: Building a Table of Content component
publishedAt: '2023-02-28'
excerpt: description of the article
tags:
  - ux
  - nextjs
headings:
  - heading-one
  - heading-two
  - heading-three
cover_image: /img/covers/toc.jpg
---
</code></pre><p>上面的片段是本文前页的样子，但有 <code>headings</code> 条目。我将用它来解释我最初的方法。如果我继续前进并映射（map through）前页，我将能够从 <code>headings</code> 数组中检索内容。</p><p>这很好，因为我将能够在 <code>TableOfContent</code> 组件中使用 <code>headings</code> 数组中的项目。这感觉很不真实，我高兴了一会儿。该组件看起来像这样：</p><pre><code class="language-jsx">import React from 'react'
import { HeadingContainer } from './style/toc.styled'

export default function TableOfContents({ headings }) {
  return (
    &lt;HeadingContainer&gt;
      &lt;p&gt;In this article&lt;/p&gt;
      &lt;ul&gt;
        {headings.map((item, index) =&gt; (
          &lt;li key={index}&gt;
            &lt;a href={`#${item}`}&gt;{item}&lt;/a&gt;
          &lt;/li&gt;
        ))}
      &lt;/ul&gt;
    &lt;/HeadingContainer&gt;
  )
}
</code></pre><p>上面的组件接收一个 headings prop，而这个 prop 又通过 Next.js 的 <code>getStaticProps()</code> 方法从 frontmatter 接收一个值。</p><pre><code class="language-jsx">export default function Blog({
  post: {
    frontmatter: { title, headings },
  },
}) {
  return (
    &lt;&gt;
      &lt;Head&gt;
        &lt;title&gt;{title}&lt;/title&gt;
      &lt;/Head&gt;
      &lt;TableOfContents headings={headings} /&gt;
    &lt;/&gt;
  )
}

// 解构参数以获得唯一的 slug
export async function getStaticProps({ params }) {
  const { slug } = params
  const { frontmatter } = await getArticleFromSlug(slug)

  return {
    props: {
      post: {
        frontmatter,
      },
    },
  }
}
</code></pre><p>如果上面的片段看起来有点混乱，你可以看看<a href="https://meje.dev/blog/how-i-built-this-blog">这篇文章</a>，我写了一个 Next.js 博客的设置过程。</p><p>该组件渲染了 frontmatter 中的项目列表，看起来很好。</p><p>但是，当我点击一个项目，希望滚动到那个部分的时候，它却没有像预期的那样工作。我遇到了一个错误，你会在下一节看到。</p><h2 id="-extract-md-headings"><strong><strong><strong>如何使用</strong></strong> <strong><strong>extract-md-headings</strong></strong></strong></h2><p>我意识到，当我点击组件中的一个项目时，浏览器用空格的编码参数对当前 slug 的 URL 进行编码——<code>%20%</code>——这反过来导致了这个问题。</p><p>我意识到这也可能是我在 <code>frontmatter</code> 中引用标题元素的方式。但这并不重要，因为我找到了一个替代方案，而且效果很好。</p><p>在我确定它完美地工作之后，我继续把这个替代方案作为一个<a href="https://npmjs.com/package/extract-md-headings">包</a>发布到 npm registery。</p><p>这个包扩展了一个函数，<code>extractHeadings()</code>，它接受一个字符串，作为一个路径，指向 markdown 文件的位置，并提取任何符合 markdown 文件中标题文本写法的文字。如果你想看看它是如何工作的，你可以看看<a href="https://github.com/kaf-lamed-beyt/extract-md-headings/blob/834ad610c80db6a367260b3ec6927c9cd2099a5c/src/index.ts#L15-L36">这里</a>的源代码。</p><p>有了这个工具，我修改了 <code>getStaticProps</code> 方法来使用这个函数。你可能会问我，为什么？嗯，因为这个包完全依赖于 Node 的 <code>fs</code> 模块，这相当于一个服务器端的脚本方法。</p><p>使用 Next.js，我们可以在页面目录中用任何一种数据获取方法，<code>getStaticProps</code>、<code>getStaticPaths</code> 和 <code>getServerSideProps</code>，进行服务器端操作：</p><pre><code class="language-jsx">import React from 'react'
import { extractHeadings } from 'extract-md-headings'

export default function Blog({
  post: {
    fileContent,
    frontmatter: { title },
  },
}) {
  return (
    &lt;&gt;
      &lt;Head&gt;
        &lt;title&gt;{title}&lt;/title&gt;
      &lt;/Head&gt;
      &lt;TableOfContents headings={fileContent} /&gt;
    &lt;/&gt;
  )
}

export async function getStaticProps({ params }) {
  const { slug } = params
  const { frontmatter } = await getArticleFromSlug(slug)
  const mdxContent = extractHeadings(`/path/to/where/${slug}.mdx`)

  return {
    props: {
      post: {
        frontmatter,
        fileContent: mdxContent,
      },
    },
  }
}
</code></pre><p>现在 <code>[slug].js</code> 页面通过 <code>TOC</code> 组件的 <code>heading</code> prop 知道了 <code>fileContent</code> 的情况。所以我需要修改它，使它能适应函数返回的属性。</p><pre><code class="language-jsx">import React from 'react'
import { HeadingContainer } from './style/toc.styled'

export default function TableOfContents({ headings }) {
  return (
    &lt;HeadingContainer&gt;
      &lt;p&gt;In this article&lt;/p&gt;
      &lt;ul&gt;
        {headings.map(({ slug, title, id }) =&gt; (
          &lt;li key={id}&gt;
            &lt;a href={`#${slug}`}&gt;{title}&lt;/a&gt;
          &lt;/li&gt;
        ))}
      &lt;/ul&gt;
    &lt;/HeadingContainer&gt;
  )
}
</code></pre><p>目前，该组件只是渲染了从函数返回的数组中的项目列表，没有交互性，没有办法跟踪哪个元素是活动的，还有很多东西我暂时还没能添加。</p><h2 id="--1">如何添加基于点击和滚动的状态</h2><p>如果说我喜欢 React 的什么，那就是它追踪状态的能力。我已经看到这在其他文档平台上是如何工作的——当你点击一个项目时，它就变成活动的，当你滚动到有标题标签的部分时，它就变成活动的。</p><p>很多人都有不同的方法来监控这些状态。我选择了最简单的方法——改变颜色——因为，像往常一样，“我不喜欢压力”。在我的组件的用户界面中，默认的文本颜色有点灰，所以当它被激活时，它就变成白色。</p><p>我先介绍一下我用 <code>useState</code> hook、一些 DOM API 和 <code>getBoundingClientRect</code> web API 对组件进行修改的代码片段。内容挺多——我知道，但是，我试着把它简单地分解一下。</p><p>当我们使用 <code>useState</code> hook 时，有一个默认值——一个布尔值、字符串或数字，这是一个常见的方法。在下面的片段中，该组件使用 <code>headings</code> prop 来检查数组的长度是否为空，是否大于零，并将组件的默认状态设置为第一个元素的默认状态。</p><pre><code class="language-js">const [active, setActive] = React.useState(
  headings.length &gt; 0 ? headings[0].slug : ''
)
</code></pre><p>如果数组是空的，没有元素会有活动状态的样式。现在，如果你在列表元素中放置一个 <code>onClick</code> 属性——就像我做的那样——并传递 <code>slug</code> 作为参数，它将切换你在 <code>style</code> 属性中写的样式。</p><pre><code class="language-jsx">&lt;li
  key={index}
  onClick={() =&gt; setActive(slug)}
  style={{
    color: active === slug ? '#fff' : '',
  }}
&gt;
  &lt;a href={`#${slug}`}&gt;{title}&lt;/a&gt;
&lt;/li&gt;
</code></pre><p>处理滚动状态需要使用 React 的 <code>useEffect</code> hook，因为它包含了所有的生命周期方法——<code>componentDidMount()</code>、<code>componentDidMount()</code> 和 <code>componentWillUnmount()</code>。在这里，我决定通过用 DOM <code>EventTarget</code> 接口监听本地滚动事件来跟踪滚动状态。</p><p>下面的函数 <code>handleScroll</code> 通过对对象的 <code>slug</code> 属性进行解构，映射我们从 <code>extractHeadings()</code> 函数中得到的结果。它继续用 <code>getElementById</code> 返回所有包含适当 <code>id</code> 属性的元素，并将其值分配给 <code>headingElements</code>。</p><pre><code class="language-js">const handleScroll = () =&gt; {
  const headingElements = headings.map(({ slug }) =&gt;
    document.getElementById(slug)
  )
  const visibleHeadings = headingElements.filter((el) =&gt;
    isElementInViewport(el)
  )
  if (visibleHeadings.length &gt; 0) {
    setActive(visibleHeadings[0].id)
  }
}
</code></pre><p>还是在这个函数中，从 <code>headingElements</code> 数组中过滤出 <code>visibleElements</code>，<code>isElementInViewport</code> 函数被用来检查哪个标题元素当前在视口中——这可以通过 <code>getBoundingClientRect</code> 实现，我很快会讲到。</p><p>该函数以一个条件结束，如果可见标题的长度大于 0，则设置一个活动元素。</p><p>现在，我可以继续把这个函数包在 Effect 中，启动滚动事件的清理，并在依赖数组中传递 <code>headings</code> prop。然后，只有当 <code>headings</code> prop 发生变化时，才会触发 Effect。</p><pre><code class="language-js">React.useEffect(() =&gt; {
  const handleScroll = () =&gt; {
    const headingElements = headings.map(({ slug }) =&gt;
      document.getElementById(slug)
    )
    const visibleHeadings = headingElements.filter((el) =&gt;
      isElementInViewport(el)
    )
    if (visibleHeadings.length &gt; 0) {
      setActive(visibleHeadings[0].id)
    }
  }

  document.addEventListener('scroll', handleScroll)

  // 通过删除事件监听器来清理 effect
  return () =&gt; {
    document.removeEventListener('scroll', handleScroll)
  }
}, [headings])
</code></pre><p><code>isElementInViewport</code> 是这个功能的重中之重。该函数接受一个元素 <code>el</code> 作为参数，并检查其边界矩形（这再次证明了网络上的盒子原则是正确的）是否在浏览器的视口内。</p><pre><code class="language-js">const isElementInViewport = (el) =&gt; {
  const rect = el.getBoundingClientRect()
  return (
    rect.top &gt;= 0 &amp;&amp;
    rect.left &gt;= 0 &amp;&amp;
    rect.bottom &lt;=
      (window.innerHeight || document.documentElement.clientHeight) &amp;&amp;
    rect.right &lt;= (window.innerWidth || document.documentElement.clientWidth)
  )
}
</code></pre><p>借助 <code>getBoundingClientRect</code> web API，这是可能的。该方法返回一个对象，包含元素的上、左、下和右边缘相对于视口的坐标。</p><p>当 <code>getBoundingClientRect</code> 被调用时，它返回一个包含特定标题元素相对于视口的上、左、下和右边缘坐标的对象。</p><p>在这个功能中，相对于视口的元素是使用 <code>getElementById</code> 方法检索到的标题元素。</p><p>如果顶部和左侧坐标大于或等于零，底部和右侧坐标分别小于或等于视口的高度和宽度，该函数返回 <code>true</code>。</p><p>为了使该函数返回 <code>true</code>，我们必须得到视口的高度和宽度的值。这就是为什么用 <code>window.innerHeight</code> 和 <code>window.innerWidth</code> 或 <code>documentElement.clientHeight</code> 和 <code>documentElement.clientWidth</code> 来比较这些值是很方便的。</p><h2 id="-intersectionobserver-">为什么会有压力？IntersectionObserver 解决了这个问题</h2><p>我知道走 <code>intersectionObserver</code> 的路线会为我减少很多压力。但是，我还是选择了这种方法，因为我想了解其他人是如何构建这个功能的。</p><p>我想你也可以用一个 <code>intersectionObserver</code> 包来监控 React 应用程序中的滚动事件，所以你可能根本不需要走这条路。但我想分享一些我决定使用这个 API 而不是 <code>IntersectionObserver</code> 的原因。</p><p>就准确性而言，<code>getBoundingClientRect</code> 返回元素相对于视口的更精确的位置，而 <code>IntersectionObserver</code> 使用基于元素边界盒的近似值。</p><p>这意味着 <code>getBoundingClientRect</code> 在某些用例中可以更精确，比如当你需要在元素进入视口时立即触发一个动作——就像我们在组件中改变列表项的活动状态。</p><p>在浏览器兼容性方面，<code>IntersectionObserver</code> 是一个相对较新的 API，其他浏览器可能不支持它。但是，另一方面，<code>getBoundingClientRect</code> 被现代浏览器广泛支持。</p><p>与 <code>getBoundingClientRect</code> 相比，<code>IntersectionObserver</code> 的一个优势是在性能方面。这是因为该 API 使用了一种优化的算法，当你跟踪这么多元素时，它可以最大限度地减少检测相交状态变化所需的工作量。</p><p><code>getBoundingClientRect</code> API 不能处理这么多元素。</p><h2 id="--2"><strong><strong><strong>总结</strong></strong></strong></h2><p>我知道，很多人还是喜欢使用 <code>intersectionObserver</code>。但是，我决定采用另一种方法，因为它让我看到了 <code>intersectionObserver</code> 本身是如何在后台工作的，而且最重要的是，它适合我的用例。</p><p>这就是 TOC 组件的逻辑，没有标记。如果你想的话，你可以复制并使用它。</p><pre><code class="language-jsx">import React from 'react'
import { HeadingContainer } from './style/toc.styled'

const TableOfContents = ({ headings }) =&gt; {
  const [active, setActive] = React.useState(
    headings.length &gt; 0 ? headings[0].slug : ''
  )

  React.useEffect(() =&gt; {
    const handleScroll = () =&gt; {
      const headingElements = headings.map(({ slug }) =&gt;
        document.getElementById(slug)
      )
      const visibleHeadings = headingElements.filter((el) =&gt;
        isElementInViewport(el)
      )
      if (visibleHeadings.length &gt; 0) {
        setActive(visibleHeadings[0].id)
      }
    }

    document.addEventListener('scroll', handleScroll)
    return () =&gt; {
      document.removeEventListener('scroll', handleScroll)
    }
  }, [headings])

  const isElementInViewport = (el) =&gt; {
    const rect = el.getBoundingClientRect()
    return (
      rect.top &gt;= 0 &amp;&amp;
      rect.left &gt;= 0 &amp;&amp;
      rect.bottom &lt;=
        (window.innerHeight || document.documentElement.clientHeight) &amp;&amp;
      rect.right &lt;= (window.innerWidth || document.documentElement.clientWidth)
    )
  }

  return // component markup
}

export default TableOfContents
</code></pre><p>如果你读到这里，请分享这篇文章，谢谢！如果你想深入了解，你也可以阅读关于 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect">getBoundingClientRect() web API</a> 的资料。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 小游戏开发：使用 React 和 Redux Tool Kit 实现俄罗斯方块 ]]>
                </title>
                <description>
                    <![CDATA[ 俄罗斯方块是一款经典小游戏。通过从零开发一款小游戏，我们可以更好地了解开发使用的语言，同时也得到更强烈的“所见即所得”的反馈，所以开发小游戏会是不错的练手项目。 从一个主力语言是JavaScript的开发者角度来分析俄罗斯方块，不同形状的方块的下落、移动、消除其实是 复杂的状态管理和组件通信 。这不刚好就是React和Redux的射程范围嘛！（并没有说它们适合游戏开发） 我将通过这篇博文拆解实现俄罗斯方块的一些核心逻辑，以及Redux Tool Kit [https://redux-toolkit.js.org/]的使用。 你可以在我的Github仓库 [https://github.com/PapayaHUANG/react-redux-tetris]上找到项目代码，也可以通过 Github Page [https://papayahuang.github.io/react-redux-tetris/]来试玩这个游戏。 本文涉及的技术  * JavaScript数组高阶函数  * 使用Redux Tool Kit对React单页面项目进行状态管理  * 使用request ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/build-tetris-game-with-react-and-redux-tool-kit/</link>
                <guid isPermaLink="false">63fcae780687e3060be26c4b</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Redux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Wed, 01 Mar 2023 03:10:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/03/nik-lUbIun4IL38-unsplash--1-.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>俄罗斯方块是一款经典小游戏。通过从零开发一款小游戏，我们可以更好地了解开发使用的语言，同时也得到更强烈的“所见即所得”的反馈，所以开发小游戏会是不错的练手项目。</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/----2023-02-28-14.36.54.gif" alt="游戏动态展示" width="340" height="608" loading="lazy"></p>
<p>从一个主力语言是JavaScript的开发者角度来分析俄罗斯方块，不同形状的方块的下落、移动、消除其实是 <em>复杂的状态管理和组件通信</em>。这不刚好就是React和Redux的射程范围嘛！（并没有说它们适合游戏开发）</p>
<p>我将通过这篇博文拆解实现俄罗斯方块的一些核心逻辑，以及<a href="https://redux-toolkit.js.org/">Redux Tool Kit</a>的使用。</p>
<p>你可以在<a href="https://github.com/PapayaHUANG/react-redux-tetris">我的Github仓库</a>上找到项目代码，也可以通过<a href="https://papayahuang.github.io/react-redux-tetris/">Github Page</a>来试玩这个游戏。</p>
<h2 id="">本文涉及的技术</h2>
<ul>
<li>JavaScript数组高阶函数</li>
<li>使用Redux Tool Kit对React单页面项目进行状态管理</li>
<li>使用<code>requestAnimationFrame</code>实现动画效果</li>
</ul>
<h2 id="">文件结构</h2>
<h3 id="">项目总结构</h3>
<pre><code>|-- src
    |-- App.js
	|-- index.js
	|-- store.js
	|-- features/
		|-- game/
			|-- components/
			|-- game-slice.js
	|-- styles/
	|-- utils/
</code></pre>
<p>我是通过<code>create-react-app</code>创建的项目，游戏界面及功能所有代码都在<code>src</code>路径下：</p>
<ul>
<li>我保留了<code>index.js</code>和<code>App.js</code>，同时添加了<code>store.js</code>来处理Redux store的配置代码；</li>
<li>样式全部放在了<code>styles</code>文件夹中，游戏运行的核心逻辑放到了<code>utils</code>文件夹中；</li>
<li>Redux官方推荐在使用RTK（Redux Tool Kit）时，创建一个<code>features</code>目录，并把不同的功能模块放在这个目录下，每一个功能模块包含自己的组件、reducer和其他相关文件（如与后端通信的代码），所以我在<code>features</code>目录下创建了<code>game</code>文件夹，并在其下创建了<code>components</code>目录和<code>game-slice.js</code>。</li>
</ul>
<h3 id="">组件结构</h3>
<p>我规划的组件包括：</p>
<pre><code>|-- components/
	|-- Board.js
	|-- Control.js
	|-- MessagePopup.js
	|-- NextBlock.js
	|-- ScoreBoard.js
	|-- Square.js
</code></pre>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/----.jpeg" alt="组件展示" width="727" height="1024" loading="lazy"></p>
<ul>
<li><code>Board.js</code>是主游戏区</li>
<li><code>Control.js</code>是按钮控制面板</li>
<li><code>MessagePopup.js</code>是游戏结束时弹出的消息框</li>
<li><code>NextBlock.js</code>是下一个形状提示框</li>
<li><code>ScoreBoard.js</code>显示当前分数和历史最高分数</li>
<li><code>Square.js</code>是组成主游戏区和提示框的小方块</li>
</ul>
<h2 id="ui">核心UI组件搭建的逻辑</h2>
<h3 id="">游戏区域的实现</h3>
<h4 id="">主游戏区框架的实现</h4>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/10-18---1.png" alt="10*18网格" width="499" height="809" loading="lazy"><br>
将主游戏区看作一个10*18的网格，可以通过单项数据为0的<strong>二维数组</strong>实现：</p>
<pre><code class="language-js">//src/utils/index.js

const boardDefault = () =&gt; {
    const rows = 18; //共18行
	const cols = 10; //共10列	
	const array = Array.from(Array(rows), () =&gt; Array(cols).fill(0));
	return array;
};
</code></pre>
<blockquote>
<p>注：在示意图中，我标注了x和y象限，因为之后会使用x和y的值来判断形状在主游戏区的位置。</p>
</blockquote>
<h4 id="">形状的实现</h4>
<p>既然主游戏区可以通过数组来实现，那么不同形状的方块也可以通过数组来实现：</p>
<p>设定每一种形状都用一个4*4的网格作为容器：</p>
<pre><code class="language-js">[

	[0, 0, 0, 0],
	[0, 0, 0, 0],
	[0, 0, 0, 0],
	[0, 0, 0, 0],

],
</code></pre>
<p>在俄罗斯方块游戏中，不同的形状的颜色不同，所以可以用整数<code>1,2,3,4...</code>来区分它们。</p>
<p>使用整数的<strong>另一个好处</strong>是可以和<strong>样式绑定</strong>，先提前设置好每个颜色对应的编号：</p>
<pre><code class="language-css">:root {
	--color-0: #282c34;
	--color-1: #ff6600;
	--color-2: #eec900;
	...
	--color-7: #ff0000;
}

.color-0 {
	background-color: var(--color-0);
}

.color-1 {
	background-color: var(--color-1);
}

...
  
.color-7 {
	background-color: var(--color-7);
}
</code></pre>
<p>然后就可以通过数字<code>1,2,3,4...</code>在前文的4*4网格数组中的排列来表达不同的形状了，需要注意的是每个形状还有旋转后的样式也要考虑进去。</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/--2023-02-28-15.55.33.png" alt="长条形状" width="124" height="107" loading="lazy"></p>
<p>比方说一个长条形状及它旋转后可以表示为：</p>
<pre><code class="language-js">//I

[
	[
		[0, 0, 0, 0],
		[1, 1, 1, 1],
		[0, 0, 0, 0],
		[0, 0, 0, 0],
	],

	[
		[0, 1, 0, 0],
		[0, 1, 0, 0],
		[0, 1, 0, 0],
		[0, 1, 0, 0],
	],
],
</code></pre>
<ul>
<li>把单个形状及其旋转后的样式放在同一个数组中，引用时就可以通过<strong>索引号</strong>来获得形状的旋转效果。</li>
<li>再把所有形状的数组汇总到一个数组中，这样通过<strong>索引号</strong>就把表示颜色的数字和形状挂钩了。</li>
</ul>
<h3 id="">形状到主游戏区的映射</h3>
<p>我们完成了主游戏区和形状分别的实现，那么当游戏进行时，如何把形状映射到主游戏区呢？</p>
<p>在React中，每一次渲染都相当于一次快照，那么就可以把游戏进行时，形状在主游戏区域的每一次移动都看作独立的一次<em>形状数组到主游戏区数组的映射</em>：</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/03/--2023-03-01-09.54.06.png" alt="快照" width="276" height="385" loading="lazy"><br>
<img src="https://chinese.freecodecamp.org/news/content/images/2023/03/----.jpeg" alt="快照数组" width="734" height="1024" loading="lazy"></p>
<p>具像化我们映射过程就是从以上第一幅图到第二幅图。</p>
<p>代码实现如下：</p>
<pre><code class="language-js">//src/features/game/components/Board.js

//这里的board变量是10*18的由0组成的二位数组
const boardSquare = board.map((rowArray, row) =&gt; {
	return rowArray.map((square, col) =&gt; {
	//col（0-9）row（0-17)
	const blockX = col - x;
	const blockY = row - y;
	//square为0
	let color = square;
	//生成移动的block的颜色
	if (
		blockX &gt;= 0 &amp;&amp;
		blockX &lt; block.length &amp;&amp;
		blockY &gt;= 0 &amp;&amp;
		blockY &lt; block.length
	) {
    //这里的block变量引用了从函数外部取得的表示形状的数组
		color = block[blockY][blockX] === 0 ? color : blockColor;
	}
	//生成key
	const k = row * board[0].length + col;
	return &lt;Square key={k} color={color} /&gt;;
	});
});
</code></pre>
<p>让我们看看上面代码发生了什么：</p>
<ul>
<li><code>x</code>,<code>y</code>是形状起始的横轴（行）和纵轴（列）的坐标，我设定的初始值为<code>x=4, y=-5</code>，这样每一个形状的初始位置就位于主游戏区差不多正上方，随着形状的移动，这两个值也会发生变化；</li>
<li><code>boardSquare</code>函数实际上做了两件事：将我们用数组表达的10*8主游戏区展示到UI；根据主游戏区纵坐标（0-17）、横坐标（0-9）与<code>x</code>、<code>y</code>的差值来定位，将形状数组映射到主游戏区；</li>
<li>首先通过一组嵌套结构的<code>Array.map()</code>的方法，遍历主游戏区所有网格，并将所有网格的<code>color</code>设置为<code>0</code>；</li>
<li>在遍历主游戏区的同时，通过blockX和blockY坐标来锁定位于形状数组内部的方块，如果它的值不为<code>0</code>的话，就返回对应数字的颜色；</li>
<li>因为React要求遍历的组件包含一个独一无二的<code>key</code>，所以通过<code>row * board[0].length + col</code>生成key。</li>
</ul>
<h2 id="">判断边界的逻辑</h2>
<h3 id="">移动范围边界</h3>
<p>形状在主游戏区的移动遵守一定的规则：</p>
<ol>
<li>左右下边都不可越界</li>
<li>如果在主游戏区碰到其他的形状，也不得移动。</li>
</ol>
<p>我们只用检查形状内部每一个方块是否符合以上规则，就可以判断形状是否可以移动了。</p>
<p>此处还需要注意的是，每一形状都被放置在4*4的网格中，所以要排除方块值为<code>0</code>情况：</p>
<pre><code class="language-js">//src/utils/index.js

const canMoveTo = (shape, board, x, y, rotation) =&gt; {
	const currentShape = shapes[shape][rotation];
	for (let row = 0; row &lt; currentShape.length; row++) {
		for (let col = 0; col &lt; currentShape[row].length; col++) {
			if (currentShape[row][col] !== 0) {
				const proposedX = col + x;
				const proposedY = row + y;
			if (proposedY &lt; 0) {
				continue;
				}
			const possibleRow = board[proposedY];
			if (possibleRow) {
				if (
					possibleRow[proposedX] === undefined ||
					possibleRow[proposedX] !== 0
					) {
						return false;
					}
				} else {
				//超越底线
						return false;
					}
				}
			}
		}
	//默认可以移动
	return true;
};
</code></pre>
<ul>
<li>使用<code>canMoveTo</code>函数来判断是否可以移动，函数接受<code>shape</code>（形状数组）、<code>board</code>（主游戏区数组）、<code>x,y</code>（形状定位坐标）、<code>rotation</code>（旋转索引号）作为参数，返回布尔值。</li>
<li>首先默认可以移动；</li>
<li>利用两个嵌套<code>Array.map()</code>遍历形状数组内部的方块，如果方块的值为非<code>0</code>，就根据<code>x</code>和<code>y</code>的值来判断方块位于游戏主区的位置，即<code>proposedX</code>和<code>proposedY</code>；</li>
<li>因为形状的初始情况是位于游戏主区上方，不在主区内，所以存在<code>proposedY</code>小于<code>0</code>的情况，这个时候继续遍历就好；</li>
<li>根据<code>propsedY</code>可以推断出方块位于主游戏区的行数<code>possibleRow</code>，如果不存在<code>possibleRow</code>的话，则说明方块已经超越了主游戏区的<strong>底边</strong>，返回<code>false</code>；</li>
<li>用<code>possibleRow[proposedX]</code>来定位形状方块位于主游戏区该行的哪一个方块，如果值不为<code>0</code>或者为<code>undefined</code>的话，说明主游戏区上<strong>已经有其他形状的方块</strong>或者<strong>超越主游戏区左右边界</strong>，返回<code>false</code>。</li>
</ul>
<h3 id="">消除条件</h3>
<p>当主游戏区一整行填满形状方块就会消除，放在数组的框架下思考，就是检查一行是否全不为<code>0</code>。如果符合条件就删除一整行，然后在数组最开始重新添加一行：</p>
<pre><code class="language-js">//src/utils/index.js

const checkRows = (board) =&gt; {
	for (let row = 0; row &lt; board.length; row++) {
	//检查是否一整行都不为'0'
		if (board[row].indexOf(0) === -1) {
		//如果是，删除这一行
				board.splice(row, 1); 
				//同时在数组最开始添加一行新的数组
				board.unshift(Array(10).fill(0));
			}
	}
}
</code></pre>
<h2 id="">状态管理</h2>
<h3 id="reduxtoolkit">使用Redux Tool Kit</h3>
<p>在这个应用中，我使用Redux Tool Kit（后文简称RTK）来实现状态管理。</p>
<p>RTK对比传统的Redux来说的<strong>优势</strong>在于：</p>
<ol>
<li>更少的样板代码</li>
<li>RTK通过Immer库来实现不可变数据，提高应用程序的性能。</li>
</ol>
<h4 id="slice">创建Slice</h4>
<p>在RTK中，reducer和action集合到slice中，可以通过引用<code>createSlice</code>来实现：</p>
<pre><code class="language-js">//src/features/game/game-slice.js

import {createSlice} from '@reduxjs/toolkit'

const gameSlice = createSlice({
	name:'game',
	initialState: {...},
	reducer: {
		rotate:(state, action) =&gt;{
			...
			},
		moveRight: (state, action) =&gt;{
			...
			},
			...
		},
})

export const {rotate, moveRight} = gameSlice.actions
export default gameSlice.reducer;
</code></pre>
<blockquote>
<p>注：在传统方式中若要修改不可变数据，需要手动复制原始数据，并在副本上进行修改，由于immer库的存在，在编写reducer的时候，可以直接修改<code>state</code>，详细代码可以查看github仓库。</p>
</blockquote>
<h4 id="store">创建store</h4>
<pre><code class="language-js">//src/store.js

import { configureStore } from '@reduxjs/toolkit';
import gameReducer from './features/game/game-slice';

export const store = configureStore({ reducer: { game: gameReducer}})
</code></pre>
<h4 id="">将数据提供给应用</h4>
<pre><code class="language-js">//src/index.js

import { Provider } from 'react-redux';
import { store } from './store';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
	&lt;React.StrictMode&gt;
		&lt;Provider store={store}&gt;
			&lt;App /&gt;
		&lt;/Provider&gt;
	&lt;/React.StrictMode&gt;
);
</code></pre>
<h4 id="">使用数据</h4>
<pre><code class="language-js">//src/features/game/components/Control.js

import { useSelector, useDispatch } from 'react-redux';
import { rotate } from '../game-slice';

export default function Controls() {
	const dispatch = useDispatch();
	const isRunning = useSelector((state) =&gt; state.game.isRunning);
    const gameOver = useSelector((state) =&gt; state.game.gameOver);

	const handleRotate = () =&gt; {
		if (!isRunning || gameOver) return;
		dispatch(rotate());
	};

	return (
		&lt;div className="controls"&gt;
		{/* rotation */}
			&lt;button
				disabled={!isRunning || gameOver}
				className="control-button"
				onClick={handleRotate}
			&gt;						
			 Rotate
			&lt;/button&gt;
</code></pre>
<h3 id="">下落动画的实现</h3>
<h4 id="deltatime">Delta Time</h4>
<p>Delta time（Δt）是一个计算两个时间点之间经过的时间差的量。在游戏开发中，delta time 指的是每一帧之间的时间间隔。</p>
<p>实现方块的下落动画，会运用到<code>delta time</code>。</p>
<p>这里我们需要计算两次调用<code>requestAnimationFrame</code>的时间差的值，如果累计差值比我们设定的游戏速度要大，就触发<code>moveDown</code>action。</p>
<p>形状的下落实际上就是在一定时间间隔下更新画面（触发moveDown这个action），不就是定时器嘛！</p>
<h4 id="requestanimationframeuserefuseeffect">使用<code>requestAnimationFrame</code>、<code>useRef</code>和<code>useEffect</code>实现定时器的原因</h4>
<ul>
<li><code>requestAnimationFrame</code></li>
</ul>
<p>一般想到定时器会想到<code>setInterval</code>和<code>setTimeout</code>，但实际上使用<code>requestAnimationFrame</code>的性能更好。</p>
<p>这是因为<code>requestAnimationFrame</code>更符合屏幕的刷新率，通常屏幕的刷新率是60Hz，也就意味着浏览器每间隔16.7毫秒（1000毫秒/60）就会更新一次页面。<code>requestAnimationFrame</code>的回调函数会在浏览器准备好下一帧时调用，从而确保每一帧都能够在屏幕刷新之前完成绘制。</p>
<p>但<code>setTimeout</code>和<code>setInterval</code>的时间并不精准，可能会出现卡顿、延迟等性能问题。</p>
<ul>
<li><code>useRef</code></li>
</ul>
<p>React中的函数组件就像一个快照，每次渲染完成后值就会丢失。所以如果需要让函数“记住”值的话，就需要使用额外的钩子。</p>
<p>在俄罗斯方块游戏中，我们需要对形状下落的帧进行记录，这个值不受组件的生命周期影响，因此需要<code>useRef</code>。</p>
<ul>
<li><code>useEffect</code></li>
</ul>
<p>游戏中只需要在特定时间调用控制下落的函数，并且这个过程是<code>Board.js</code>组件的一个副作用，因此需要使用<code>useEffect</code>。</p>
<pre><code class="language-js">//src/utils/useTimer.js

import { useEffect, useRef } from 'react';
import { useDispatch } from 'react-redux';

export function useTimer(flagVal, benchmark, func) {
	const requestRef = useRef();
	const lastUpdateTimeRef = useRef(0);
	const progressTimeRef = useRef(0);
	
	const dispatch = useDispatch();

	const update = (time) =&gt; {
		requestRef.current = requestAnimationFrame(update);
		if (!flagVal) return;
		if (!lastUpdateTimeRef.current) {
			lastUpdateTimeRef.current = time;
		}
	
		const deltaTime = time - lastUpdateTimeRef.current;
		progressTimeRef.current += deltaTime;
	
		if (progressTimeRef.current &gt; benchmark) {
		dispatch(func());
		progressTimeRef.current = 0;
		}
		lastUpdateTimeRef.current = time;
	};

	useEffect(() =&gt; {
		requestRef.current = requestAnimationFrame(update);
		return () =&gt; cancelAnimationFrame(requestRef.current);
	}, [flagVal]);
}
</code></pre>
<ul>
<li>使用<code>useRef</code>创建了三个引用变量<code>requestRef</code>、<code>lastUpdateTimeRef</code>和<code>progressTimeRef</code>，用于存储定时器的状态和进度。</li>
<li><code>requestRef</code>用于存储当前动画帧的引用， <code>lastUpdateTimeRef</code>用于存储上一次更新的时间戳，<code>progressTimeRef</code>用于存储已经过去的时间。</li>
<li>在<code>update</code>函数中实现定时器逻辑。</li>
<li>首先使用<code>requestAnimationFrame</code>来获取下一帧的引用，并存储到<code>requestRef.current</code>中，以实现流畅的动画效果；</li>
<li>然后通过<code>flagVal</code>的值，来决定是否退出函数；</li>
<li>接着如果没有<code>lastUpdateTimeRef.current</code>的值，就将当前的时间戳(<code>time</code>)存储到里面。</li>
<li>创建<code>deltaTime</code>，它的值为当前时间戳和<code>lastUpdateTimeRef.current</code>的时间差，将每次更新得到的<code>deltaTime</code>都添加到<code>progressTimeRef.current</code>中，记录累计过去的时间；</li>
<li>如果这个累计值大于我们设定的时间间隔<code>benchmark</code>，就执行传入的函数，执行完毕后，将累计时间清零；</li>
<li>最后将当前时间戳存储在<code>lastUpdateTimeRef.current</code>中，以便下一次调用时使用。</li>
<li>在使用这个定时器时，我们传入应用的状态属性<code>isRunning</code>作为<code>flagVal</code>来判断是否需要退出函数，<code>speed</code>作为<code>benchmark</code>来和累积值做对比，控制形状的下移动画的速度，<code>moveDown</code>作为<code>func</code>来实现方块下移的动作。</li>
</ul>
<h2 id="">总结</h2>
<p>本项目通过数组实现了基本的游戏静态画面，动画部分使用了<code>requestAnimationFrame</code>，游戏复杂的状态管理借助了更简洁的RTK。</p>
<p>如果你仔细观察项目代码，会发现我还使用了第三方库Redux Persist来存储最高分数。</p>
<p>如果你想要动手实验一下我在文章中提到的逻辑和状态管理，我准备了<a href="https://github.com/PapayaHUANG/react-redux-tetris-starter">starter仓库</a>，它包含了基础的框架和样式，你可以在此基础上搭建属于你的俄罗斯方块，当然你也可以在我的项目基础上添加新的内容，如不同的关卡（下落速度不同），将按键和键盘绑定等。</p>
<p>希望阅读这篇文章让你有所收获，祝你实验愉快！</p>
<h2 id="">代码仓库</h2>
<ul>
<li><a href="https://github.com/PapayaHUANG/react-redux-tetris">完整项目</a></li>
<li><a href="https://github.com/PapayaHUANG/react-redux-tetris-starter">仅UI部分</a></li>
<li><a href="https://papayahuang.github.io/react-redux-tetris/">试玩</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 浅析 React Hooks：论如何优雅地利用闭包 ]]>
                </title>
                <description>
                    <![CDATA[ 前言 在通关 JS 的道路上你一定也遇到过两个难缠的大 BOSS：this的指向和闭包，而且不能逃课。 在使用 React 时，如果你习惯编写 class 组件，就绕不开this；如果你使用函数组件，就一定会用到 React Hooks，而 React Hooks 又大量使用闭包。（闭包真的无处不在呢！） 本文将用代码示例，仿写一个简化版的 useState Hook，浅析闭包在 React 中的应用。当然本文的代码示例不代表 React 源码的真实情况。 前置条件 想要彻底理解本文内容需要你：  * 对 JS 闭包有大概了解。不太了解也没关系，我会在本文回顾。  * 浏览过 React Hooks 文档 [https://zh-hans.reactjs.org/docs/hooks-intro.html]，最好是使用过    React Hooks。 Let's get our hands dirty! “闭包”是什么？ 请允许我引用来自《你不知道的 JavaScript》对闭包的定义： > ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/react-hooks-how-to-elegantly-leverage-closures/</link>
                <guid isPermaLink="false">63e5d5dfe673d23d28878348</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Wed, 15 Feb 2023 01:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/02/ferenc-almasi-tvHtIGbbjMo-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="">前言</h2>
<p>在通关 JS 的道路上你一定也遇到过两个难缠的大 BOSS：<code>this</code>的指向和闭包，而且不能逃课。</p>
<p>在使用 React 时，如果你习惯编写 class 组件，就绕不开<code>this</code>；如果你使用函数组件，就一定会用到 React Hooks，而 React Hooks 又大量使用闭包。（闭包真的无处不在呢！）</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/giphy.gif" alt="voldmort laugh" width="241" height="203" loading="lazy"></p>
<p>本文将用代码示例，仿写一个简化版的 useState Hook，浅析闭包在 React 中的应用。当然本文的代码示例不代表 React 源码的真实情况。</p>
<h2 id="">前置条件</h2>
<p>想要彻底理解本文内容需要你：</p>
<ul>
<li>对 JS 闭包有大概了解。不太了解也没关系，我会在本文回顾。</li>
<li>浏览过 React Hooks <a href="https://zh-hans.reactjs.org/docs/hooks-intro.html">文档</a>，最好是使用过 React Hooks。</li>
</ul>
<p>Let's get our hands dirty!</p>
<h2 id="">“闭包”是什么？</h2>
<p>请允许我引用来自《你不知道的 JavaScript》对闭包的定义：</p>
<blockquote>
<p>当函数可以记住并访问所在的词法作用域时，就产生了闭包，即使函数是在当前词法作用域之外执行。</p>
</blockquote>
<p>第一次读到这句话的时候，我深刻地体会到了什么叫“每一个字我都认识，但是我完全不知道它说了什么”的绝望。现在回看，其实《你不知道的 JavaScript》的章节设计得很好，想要真的理解<code>闭包</code>这个概念，推荐熟读前面<code>词法作用域</code>部分，了解了作用域，如何查找变量，闭包问题也就引刃而解了。</p>
<p>对于我个人来说，开悟的那一刻是当我把<code>闭包</code>的英文<code>closure</code>转换成动词词组<code>close over</code>来理解的那一刻。<code>close</code>就是<strong>打包了</strong>当前作用域，而<code>over</code>是<strong>超越了</strong>自己的作用域，在作用域之外也可以执行。</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/mehedi-hasan-k-sh-_jFTWvKrwyY-unsplash.jpg" alt="lightbulb" width="2000" height="2667" loading="lazy"></p>
<p><em>顺便提一句，很多概念直接通过英文来理解会容易很多，另一个例子是作用域的英文：<code>scope</code>。技术书能读英文原版就读原版，比方说《JavaScript 忍者秘籍》，很好的一本书，但是中文翻译了个啥？？？</em></p>
<p>废话就说这么多，请看一个经典的代码示例：</p>
<pre><code class="language-js">function addOne() {
  let num = 1;
  return function inner() {
    num = num + 1;
    return num;
  };
}

const outer = addOne();

console.log(outer()); // 2
console.log(outer()); // 3
console.log(outer()); // 4
console.log(outer()); // 5
num = 999; // 报错
</code></pre>
<p>让我们一起看看在上面的代码片段发生了什么？</p>
<ul>
<li>当我们在调用<code>outer</code>函数的时候，实际调用的是<code>addOne</code>的内部的<code>inner</code>函数。</li>
<li><code>inner</code>函数引用了其作用域之外的<code>num</code>的值。</li>
<li><code>inner</code>函数在自己的作用域（即<code>addOne</code>函数）之外（即<code>outer</code>函数）中执行。</li>
<li>我们通过在<code>addOne</code>函数中返回<code>inner</code>函数（close），并在外部调用（over）实现了一个经典的闭包（closure）。</li>
</ul>
<p>而通过打印到控制台的结果，发现实现了两个效果：</p>
<ol>
<li><code>num</code>为私有变量，尝试在全局修改变量的值会报错。私有变量也是闭包的常用场景之一。</li>
<li>反复调用<code>outer</code>函数可以实现数字的等差累加（+1），也就是说<code>outer</code>是一个<strong>有状态的函数</strong>（stateful function）。</li>
</ol>
<h3 id="statefulfunction">闭包与有状态函数（stateful function）</h3>
<p>有状态函数就是利用<strong>闭包</strong>，使得函数拥有内部状态，这样每一次调用函数会基于上一次的结果产生不一样的结果。（可以对比纯函数的概念理解）</p>
<p>看到这里你是不是想到了什么？React 不就是利用 React Hooks 对组件进行<em>状态管理</em>嘛？！</p>
<h2 id="usestate">仿写 useState</h2>
<h3 id="usestate">编写 useState 函数</h3>
<p>那让我们进入正题，从仿写<code>useState</code>开始。</p>
<p>根据<code>useState</code>的结构，可以得出如果编写一个<code>useState</code>函数的话，需要返回一个数组，包含一个值和一个相关的 setter 函数：</p>
<pre><code class="language-js">function myUseState(initValue) {
  let _value = initValue;
  const state = _value;
  const setState = (newValue) =&gt; {
    _value = newValue;
  };
  return [state, setState];
}
</code></pre>
<p>让我们看看上面的代码片段发生了什么：</p>
<ul>
<li><code>useState</code>函数返回了一个数组，这个数组包含两个变量：<code>state</code>和一个 setter 函数<code>setState</code>。</li>
<li><code>useState</code>函数通过内部变量<code>_value</code>记住了我们传入的初始值，<code>setState</code>引用了这个值，并做修改。</li>
</ul>
<p>在真实的 React 中，我们是通过<code>React.useState</code>来调用这个 hook，那我们需要调整一下上面这个函数的写法，将函数封装在 React 中。</p>
<p>我们先写个简单的练手，把前文的<code>outer</code>函数改写一下：</p>
<pre><code class="language-js">const outer = (function addOne() {
  let num = 1;
  return function inner() {
    num = num + 1;
    return num;
  };
})();

console.log(outer()); // 2
console.log(outer()); // 3
console.log(outer()); // 4
console.log(outer()); // 5
</code></pre>
<p>利用 IIFE 将代码封装后和之前的输出结果一致，同理就可以把<code>myUseState</code>函数改写为：</p>
<pre><code class="language-js">const MyReact = (function () {
  function myUseState(initValue) {
    let _value = initValue;
    const state = _value;
    const setState = (newValue) =&gt; {
      _value = newValue;
    };
    return [state, setState];
  }
  return { myUseState };
})();
</code></pre>
<p>让我们测试一下使用：</p>
<pre><code class="language-js">const [count, setCount] = MyReact.myUseState(1);

console.log(count); //1
setCount(count + 1);
console.log(count); //1
</code></pre>
<p>为什么<code>setCount</code>失效了？让我们来分析一下调用<code>setCount(count + 1)</code>的时候发生了什么：</p>
<ul>
<li><code>setCount</code>是在外部调用了<code>setState</code>函数（over）</li>
<li><code>setState</code>可以访问和修改<code>myUseState</code>函数内部<code>_value</code>的值（close）</li>
<li><code>setState</code>是一个闭包，并且当我们在调用<code>setCount(count + 1)</code>时，确实改变了<code>_value</code>的值</li>
<li>但<code>count</code>拿到的值是返回数组的第一项，并不持续追踪<code>_value</code>值的变化</li>
</ul>
<p>让我们再回到<strong>闭包</strong>的概念：</p>
<blockquote>
<p>当函数可以记住并访问所在的词法作用域时，就产生了闭包，即使函数是在当前词法作用域之外执行。</p>
</blockquote>
<p>很容易以为误闭包记住的是值，但实际上闭包 close（记住）的是<strong>作用域</strong>。那我们只需要把<code>state</code>也修改为<code>_value</code>的闭包，让 <code>state</code> 也记住它的作用域就可以了：</p>
<pre><code class="language-js">const MyReact = (function () {
  function myUseState(initValue) {
    let _value = initValue;
    const state = () =&gt; _value; //state 为 _value 的闭包
    const setState = (newValue) =&gt; {
      _value = newValue;
    };
    return [state, setState]; //外部可以调用 state
  }
  return { myUseState };
})();
</code></pre>
<p>对应打印到控制台的代码也要修改：</p>
<pre><code class="language-js">const [count, setCount] = MyReact.myUseState(1);

console.log(count()); //1
setCount(count() + 1);
console.log(count()); //2
</code></pre>
<h3 id="">编写函数组件</h3>
<p>让我们继续：</p>
<p>在真实的 React Hook 的使用场景中，我们是在一个函数组件中调用 Hook，让我们边调整边完善：</p>
<pre><code class="language-js">function MyComponent() {
  const [count, setCount] = MyReact.myUseState(1);

  return {
    //此处应该为 JSX
  };
}
</code></pre>
<p>假设返回的 JSX 是一个可以展示<code>count</code>的值的标签，以及一个按钮，每次点击之后<code>count</code>的值加 1。</p>
<p>为了简化案例，我们把 JSX 改写为打印到控制台，这样 <code>return</code> 返回的对象就包含两个方法：</p>
<ul>
<li>一个<code>render</code>方法，在控制台输出<code>count</code>的值；</li>
<li>一个<code>click</code>方法，模拟按钮，每次调用改变<code>count</code>的值。</li>
</ul>
<pre><code class="language-js">function MyComponent() {
  const [count, setCount] = MyReact.myUseState(1);

  return {
    render: () =&gt; console.log(count),
    click: () =&gt; setCount(count + 1),
  };
}
</code></pre>
<p>这样我们就构建好了一个函数组件。</p>
<h3 id="">编写渲染方法</h3>
<p>通常我们使用<code>ReactDOM.render（要渲染的内容，在哪里渲染）</code>将函数组件渲染到页面，那么我们的仿写还差一个渲染方法：</p>
<pre><code class="language-js">const MyReact = (function () {
  function myUseState(initValue) {
    ...
    return [state, setState];
  }

  function myRender(Component){
    const _c = Component();
    _c.render();//在控制台打印状态
    return _c;
  }

  return { myUseState, myRender };
})();
</code></pre>
<p>由于在编写函数组件的时候，我们返回的<code>render</code>方法是<code>() =&gt; console.log(count)</code>，直接在控制台打印状态，所以我们对<code>myUseState</code>要稍作修改：</p>
<pre><code class="language-js">const MyReact = (function () {
  let _val; //放在最外层
  function myUseState(initVal) {
    const state = _val || initVal;//不需要使用函数
    const setState = (newVal) =&gt; {
      _val = newVal;
    };
    return [state, setState];
  }
  ...
  return { myUseState, myRender };
})();
</code></pre>
<p>现在完整的代码为：</p>
<pre><code class="language-js">const MyReact = (function () {
  let _value; //在 MyReact 最外层声明变量
  function myUseState(initValue) {
    const state = _val || initVal; //不需要使用函数
    const setState = (newValue) =&gt; {
      _value = newValue;
    };
    return [state, setState];
  }
  function myRender(Component) {
    const _c = Component();
    _c.render(); //在控制台打印状态
    return _c;
  }
  return { myUseState, myRender };
})();

function MyComponent() {
  const [count, setCount] = MyReact.myUseState(1);

  return {
    render: () =&gt; console.log(count),
    click: () =&gt; setCount(count + 1),
  };
}
</code></pre>
<p>我们来查看一下调用效果：</p>
<pre><code class="language-js">var App = MyReact.myRender(MyComponent); //1
App.click();
var App = MyReact.myRender(MyComponent); //2
</code></pre>
<p>简要分析一下在这个代码片段里发生了什么：</p>
<ul>
<li><code>MyReact.myRender</code>方法对我们编写的组件<code>MyReactComponent</code>进行调用，直接将<code>count</code>的值打印到控制台，同时也暴露了<code>click</code>方法；</li>
<li>使用<code>App.click</code>调用暴露的方法，在非<code>setCount</code>的词法作用域调用<code>setCount</code>方法（over），修改了之前的<code>_val</code>值（close）；</li>
<li>当我们再次利用<code>myRender</code>方法获取<code>count</code>值时，<code>count</code>拿到的是修改后<code>_val</code>的值，就从<code>1</code>变成了<code>2</code>。</li>
</ul>
<p>做得不错！</p>
<p>但往往我们在一个函数组件中使用不止一个<code>useState</code>：</p>
<pre><code class="language-js">function MyComponent() {
  const [count, setCount] = MyReact.myUseState(1);
  const [text, setText] = MyReact.myUseState('freeCodeCamp');

  return {
    render: () =&gt; console.log({ count, text }), //同时展现两个变量
    click: () =&gt; setCount(count + 1),
    type: (newText) =&gt; setText(newText), //假设有一个输入框
  };
}
</code></pre>
<p>打印控制台就会出现诡异的效果：</p>
<pre><code class="language-js">var App = MyReact.myRender(MyComponent); //{"count": 1, "text": 1}
App.click();
var App = MyReact.myRender(MyComponent); //{"count": 2, "text": 2}
</code></pre>
<p>这是因为我们只有<code>_val</code>这一个值来存储变量，这个时候我们就需要引入数组：</p>
<pre><code class="language-js">const MyReact = (function () {
  let hooks = [];
  let idx = 0;
  function myUseState(initVal) {
    const state = hooks[idx] || initVal;
    const _idx = idx; //记住这个值
    const setState = (newVal) =&gt; {
      hooks[_idx] = newVal; //调用 setState 修改的是创建 state 同索引所指向的值
    };
    idx++; //这样才能在 hook 数组中记录下一个值
    return [state, setState];
  }

  function myRender(Component) {
    idx = 0; //每一次渲染的时候，索引要回到 0
    const C = Component();
    C.render();
    return C;
  }
  return { myUseState, myRender };
})();
</code></pre>
<p>重新调用：</p>
<pre><code class="language-javascript">var App = MyReact.myRender(Component); //{"count": 1, "text": 'freeCodeCamp'}
App.click();
var App = MyReact.myRender(Component); //{"count": 2, "text": 'freeCodeCamp'}
App.setText('awesome!');
var App = MyReact.myRender(Component); //{"count": 2, "text": 'awesome!'}
</code></pre>
<p>这里要稍微解释一下为什么在<code>myRender</code>函数中，一定要将<code>idx</code>的值重新设置为<code>0</code>。</p>
<p>因为每一次调用<code>MyReact.myRender</code>都是<strong>独立的</strong>，如果不将索引设置为<code>0</code>的话，多次调用<code>useState</code>会将在上次索引上继续索引，这样上次索引的记录就不会被重写，每次渲染<code>Component</code>时就会返回同样的状态值和 setter 函数。</p>
<h2 id="">总结</h2>
<p>探索闭包和 React Hooks 之间的关系是一段有趣的旅程。</p>
<p>你也可以尝试仿写其他 hook 方法。</p>
<p>希望这篇文章可以启发你，在以后的打怪之路上优雅地理解和利用闭包。<br>
<img src="https://chinese.freecodecamp.org/news/content/images/2023/02/--.jpeg" alt="it's only the beginning" width="259" height="194" loading="lazy"></p>
<h2 id="">参考资料</h2>
<p><a href="https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/">Swyx - Deep dive: How do React hooks really work?</a><br>
<a href="https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e">Rudi Yardley - React hooks: not magic, just arrays</a></p>
<!--kg-card-end: markdown--><p></p><p></p><p> </p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何在 React 中创建一个过滤组件 ]]>
                </title>
                <description>
                    <![CDATA[ 过滤器组件在网站上很有用，因为它们可以帮助用户快速、轻松地找到他们需要的结果。 如果你的数据来自于 API，这一点尤其正确，因为用户无法查看你的应用程序所提供的所有内容。 在这篇文章中，我们将使用我们已经硬编码的假数据，并把它们像数组一样保存在一个名为 Data.js 的单独组件中。 我们将在这里讨论的内容：  1. 介绍  2. 创建我们的 React App  3. 使用 Hooks 从 Data.js 获取数据  4. 构建我们的 app 的 UI  5. 创建过滤器组件  6. 总结 介绍 对于这个特定的项目，我们将使用假的食物数据，其中包含几个键-值对，如该代码所示： const Data = [   {     id: "1",  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-make-a-filter-component-in-react/</link>
                <guid isPermaLink="false">63e9f11dcf34b8063af88c06</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Mon, 06 Feb 2023 00:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/02/photo-1672309046475-4cce2039f342.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-make-a-filter-component-in-react/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Make a Filter Component in React</a>
      </p><p>过滤器组件在网站上很有用，因为它们可以帮助用户快速、轻松地找到他们需要的结果。</p><p>如果你的数据来自于 API，这一点尤其正确，因为用户无法查看你的应用程序所提供的所有内容。</p><p>在这篇文章中，我们将使用我们已经硬编码的假数据，并把它们像数组一样保存在一个名为 <strong>Data.js</strong> 的单独组件中。</p><p><strong>我们将在这里讨论的内容：</strong></p><ol><li>介绍</li><li>创建我们的 React App</li><li>使用 Hooks 从 Data.js 获取数据</li><li>构建我们的 app 的 UI</li><li>创建过滤器组件</li><li>总结</li></ol><h2 id="-">介绍</h2><p>对于这个特定的项目，我们将使用假的食物数据，其中包含几个键-值对，如该代码所示：</p><pre><code class="language-json">const Data = [
  {
    id: "1",
    title: "Poha",
    category: "Breakfast",
    price: "$1",
    img: "https://c.ndtvimg.com/2021-08/loudr2go_aloo-poha_625x300_05_August_21.jpg?im=FeatureCrop,algorithm=dnn,width=620,height=350",
    desc: " Poha. Light, filling and easy to make, poha is one famous breakfast that is eaten almost everywhere in the country. And the best part is- it can be made in a number of ways. Kanda poha, soya poha, Indori poha, Nagpur Tari Poha are a few examples",
  },
  {
    id: "2",
    title: "Upma",
    category: "Breakfast",
    price: "$1",
    img: "https://c.ndtvimg.com/2021-04/37hi8sl_rava-upma_625x300_17_April_21.jpg?im=FeatureCrop,algorithm=dnn,width=620,height=350",
    desc: " A quintessential South Indian Breakfast! Made with protein-packed urad dal and semolina followed by crunchy veggies and curd, this recipe makes for a hearty morning meal. With some grated coconut on top, it gives a beautiful south-Indian flavour.",
  },
  {
    id: "3",
    title: "Cheela",
    category: "Breakfast",
    price: "$1",
    img: "https://c.ndtvimg.com/2019-05/1afu8vt8_weight-loss-friendly-breakfast-paneer-besan-chilla_625x300_25_May_19.jpg?im=FaceCrop,algorithm=dnn,width=620,height=350",
    desc: "A staple across Indian households, moong dal is widely used in a number of Indian delicacies. One such delicacy is moong dal cheela. You can also add paneer to this recipe to amp up the nutritional value and make it, even more, protein-dense",
  },
  {
    id: "4",
    title: "Channa Kulcha",
    category: "Lunch",
    price: "$1",
    img: "https://i.ndtvimg.com/i/2015-04/chana-kulcha_625x350_41429707001.jpg",
    desc: "A classic dish that never goes out of style. The quintessential chana kulcha  needs only a few ingredients - cumin powder, ginger, coriander powder, carom powder, and some mango powder, which is what gives the chana its sour and tangy taste.",
  },
  {
    id: "5",
    title: "Egg Curry",
    category: "Lunch",
    price: "$1",
    img: "https://i.ndtvimg.com/i/2017-11/goan-egg-curry_620x350_41511515276.jpg",
    desc: "Eggs are a versatile food that can be cooked for any meal of the day. From breakfast to dinner, it can be a go-to food. Here is a mildly-spiced egg curry made with garlic, onions, a whole lot of kasuri methi, fresh cream, yogurt, and fresh coriander.",
  },
  {
    id: "6",
    title: "Paneer Aachari",
    category: "Lunch",
    price: "$1",
    img: "https://i.ndtvimg.com/i/2015-04/paneer_625x350_61429707960.jpg",
    desc: "Don't get intimidated by the list of ingredients because not only are already in your kitchen cabinet, but also because all they'll need is 20 minutes of your time. Chunks of cottage cheese cooked in some exciting spices, yogurt and a pinch of sugar.",
  },
  {
    id: "7",
    title: "Fish Fry",
    category: "Dinner",
    price: "$1",
    img: "https://i.ndtvimg.com/i/2015-06/indian-dinner_625x350_41434360207.jpg",
    desc: "Get your daily dose of perfect protein. Pieces of surmai fish marinated in garlic, cumin, fennel, curry leaves, and tomatoes are pan-fried in refined oil and served hot. This fish fry recipe has a host of delectable spices used for marination giving it a unique touch.",
  },
  {
    id: "8",
    title: "Dum Alloo",
    category: "Dinner",
    price: "$1",
    img: "https://i.ndtvimg.com/i/2015-06/indian-dinner_625x350_51434362664.jpg",
    desc: "Your family will thank you for this fantastic bowl of dum aloo cooked Lakhnawi style. Take some potatoes, crumbled paneer, kasuri methi, butter, onions, and some ghee.",
  },
  {
    id: "9",
    title: "Malai Kofta",
    category: "Dinner",
    price: "$1",
    img: "https://i.ndtvimg.com/i/2017-10/makhmali-kofte_620x350_51508918483.jpg",
    desc: "A rich gravy made of khus khus, coconut and milk that tastes best with koftas made from khoya. This velvety and creamy recipe will leave you licking your fingers. Makhmali kofte can be your go-to dish for dinner parties as this is quite different from other kofta recipes and extremely delicious.",
  },
  {
    id: "10",
    title: "Malai Kofta",
    category: "Snaks",
    price: "$1",
    img: "https://i.ndtvimg.com/i/2017-10/makhmali-kofte_620x350_51508918483.jpg",
    desc: "A rich gravy made of khus khus, coconut and milk that tastes best with koftas made from khoya. This velvety and creamy recipe will leave you licking your fingers. Makhmali kofte can be your go-to dish for dinner parties as this is quite different from other kofta recipes and extremely delicious.",
  },
];
 
export default Data;
</code></pre><p>在这些键-值对中，我们也有一个类别，它将用于过滤结果。</p><p>我们将使用 bootstrap 作为这个项目的 CDN，用于设计我们的应用程序的样式。</p><p>在学习本教程之后，你应该知道更多关于如何在 React 中制作和导入组件，如何动态地使用数据，以及最重要的是如何在父子组件之间传递和使用 props。</p><h2 id="-react-">如何创建我们的 React 组件</h2><p>创建一个 React 应用程序非常简单——只需在任何一个你喜欢的 IDE 中进入你的工作目录，在终端输入以下命令：</p><pre><code>npx create-react-app react-filter-app</code></pre><p>如果你不确定如何正确设置 create-react-app 项目，你可以参考 <a href="https://create-react-app.dev/docs/getting-started/">create-react-app-dev</a> 的官方指南。</p><p>设置完成后，在同一终端运行 <code>npm start</code>，启动将要托管我们的 React 应用的 localhost:3000。我们也可以在那里看到我们所有的变化。</p><h2 id="-hooks-data-js-">如何使用 hooks 从 Data.js 获取数据</h2><p>现在我们已经成功地设置了 React 项目，现在是时候从 Data.js 中获取我们的数据并在用户界面中使用它了。</p><p>为此，我们首先需要在 <strong>App.js</strong> 组件中导入数据，然后使用 useState hook 来管理数据的状态。</p><pre><code class="language-javascript">import React, { useState } from "react";
import Data from "./Data";
import Card from "./Card";
 
const App = () =&gt; {
  const [item, setItem] = useState(Data);
  return (
    &lt;&gt;
      &lt;div className="container-fluid"&gt;
        &lt;div className="row"&gt;
          &lt;h1 className="col-12 text-center my-3 fw-bold"&gt;Our Menu&lt;/h1&gt;
          &lt;Card item={item} /&gt; // UI Component
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};
 
export default App;
</code></pre><h2 id="--1">如何构建应用程序的用户界面</h2><p>现在数据已经存储在一个变量中，我们可以在应用程序中自由使用变量。我们可以开始制作用户界面了。</p><p>UI 将包含 <a href="https://getbootstrap.com/docs/5.0/components/card/">bootstrap 卡片</a>，我们将使用 map 函数动态地制作这些卡片。</p><p>我们将为卡片制作一个不同的组件。正如你在上面的代码中所看到的，我们将其命名为 <strong>Card.js</strong>，并将其导入。我们还将 <strong>item</strong> 作为 props 传递，这样我们就可以在卡片组件中使用存储在 item 中的数据。</p><p>这个组件将包含我们所有的卡片和数据。我们将使用 <strong>map 函数</strong>在应用程序中动态地显示这些卡片和数据。</p><pre><code class="language-javascript">import React from "react";
 
const Card = ({ item }) =&gt; {            
           // destructuring props
  return (
    &lt;&gt;
      &lt;div className="container-fluid"&gt;
        &lt;div className="row justify-content-center"&gt;
          {item.map((Val) =&gt; {
            return (
              &lt;div
                className="col-md-4 col-sm-6 card my-3 py-3 border-0"
                key={Val.id}
              &gt;
                &lt;div className="card-img-top text-center"&gt;
                  &lt;img src={Val.img} alt={Val.title} className="photo w-75" /&gt;
                &lt;/div&gt;
                &lt;div className="card-body"&gt;
                  &lt;div className="card-title fw-bold fs-4"&gt;
                    {Val.title} &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;--&amp;nbsp;&amp;nbsp;
                    {Val.price}
                  &lt;/div&gt;
                  &lt;div className="card-text"&gt;{Val.desc}&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            );
          })}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};
 
export default Card;
</code></pre><p>我们的应用程序有 10 张卡片，看起来是这样的：</p><figure class="kg-card kg-image-card"><img src="https://lh5.googleusercontent.com/dcZ-3eTALXbuRdMYsDgy672KsDcuN7D--QbHOl5_2xNjocun5-zAONVPFqD8txXpVvbyKNNBV6rjEi5JAIceQzMy-K-D1OOOjWkKs59EzlRzf-VBkjwz3LxY2I8E4FBL6Bn4vrf5" class="kg-image" alt="dcZ-3eTALXbuRdMYsDgy672KsDcuN7D--QbHOl5_2xNjocun5-zAONVPFqD8txXpVvbyKNNBV6rjEi5JAIceQzMy-K-D1OOOjWkKs59EzlRzf-VBkjwz3LxY2I8E4FBL6Bn4vrf5" width="600" height="400" loading="lazy"></figure><h2 id="--2">如何制作过滤器组件</h2><p>我们有很多方法可以使用过滤组件来过滤掉用户从搜索结果中得到的数据。但在这里，我们将为此目的制作按钮，它将根据食物的类别过滤出数据——如早餐、午餐、晚餐和零食。</p><p>我们必须声明一个新的数组，它将只包含关键类别的值，并使用 map 方法显示它们。</p><pre><code class="language-javascript">// 扩展操作符将显示我们数据中的类别部分的所有值，而 Set 将只允许显示每种类型的单一值

  const menuItems = [...new Set(Data.map((Val) =&gt; Val.category))];</code></pre><p>我们在这里使用 <strong>spread 运算符</strong>，这样我们通过显示上述数组得到的每个值都有相同的 UI，同时也是为了将所有 10 个类别显示为按钮。</p><p>我们使用 <code>Set()</code> 值，这样只有 3 或 4 个值会被显示出来，同时也确保没有重复的值。</p><p>我们将为这些按钮创建一个新的组件，它将通过 map 方法动态显示。但这次我们将使用我们新形成的数组，因为它将所有的类别存储在一个数组中，并且由于 <code>Set()</code>，将只显示一次。</p><pre><code class="language-javascript">import React from "react";
import Data from "./Data";
 
const Buttons = ({ setItem, menuItems }) =&gt; {
  return (
    &lt;&gt;
      &lt;div className="d-flex justify-content-center"&gt;
        {menuItems.map((Val, id) =&gt; {
          return (
            &lt;button
              className="btn-dark text-white p-1 px-2 mx-5 btn fw-bold"
              key={id}
            &gt;
              {Val}
            &lt;/button&gt;
          );
        })}
        &lt;button
          className="btn-dark text-white p-1 px-3 mx-5 fw-bold btn"
          onClick={() =&gt; setItem(Data)}
        &gt;
          All
        &lt;/button&gt;
       &lt;/div&gt;
    &lt;/&gt;
  );
};
 
export default Buttons;
</code></pre><p>把这个按钮组件放在你想显示按钮的地方。在我们的例子中，我们在 app.js 中的卡片组件上方显示了按钮。</p><figure class="kg-card kg-image-card"><img src="https://lh5.googleusercontent.com/gX2PTVbyYQIJ-6o_WvhHZVucTJwEZhQz0moqf7GZoC68fcgC2iORyLyqRILAmhQn-e_SQy172o1_BgeLMidY69Jm3UCAXtRBiP-fNwFf50VaJPj8_54SjjlngVvCun_EaOVG-DRh" class="kg-image" alt="gX2PTVbyYQIJ-6o_WvhHZVucTJwEZhQz0moqf7GZoC68fcgC2iORyLyqRILAmhQn-e_SQy172o1_BgeLMidY69Jm3UCAXtRBiP-fNwFf50VaJPj8_54SjjlngVvCun_EaOVG-DRh" width="600" height="400" loading="lazy"></figure><p>是时候在这些按钮中添加一个过滤器了，这样用户就可以根据类别过滤出菜肴。</p><pre><code class="language-javascript">const filterItem = (curcat) =&gt; {
    const newItem = Data.filter((newVal) =&gt; {
      return newVal.category === curcat; 
        	// 比较类别显示数据
    });
    setItem(newItem);
  };</code></pre><p>过滤器方法根据该对象的类别过滤出数据。</p><p>使用 <code>onClick()</code> 事件处理程序，我们可以将这个过滤器附加到按钮上：</p><pre><code class="language-javascript">import React from "react";
import Data from "./Data";
 
const Buttons = ({ filterItem, setItem, menuItems }) =&gt; {
  return (
    &lt;&gt;
      &lt;div className="d-flex justify-content-center"&gt;
        {menuItems.map((Val, id) =&gt; {
          return (
            &lt;button
              className="btn-dark text-white p-1 px-2 mx-5 btn fw-bold"
              onClick={() =&gt; filterItem(Val)}
              key={id}
            &gt;
              {Val}
            &lt;/button&gt;
          );
        })}
        &lt;button
          className="btn-dark text-white p-1 px-3 mx-5 fw-bold btn"
          onClick={() =&gt; setItem(Data)}
        &gt;
          All
        &lt;/button&gt; 
       &lt;/div&gt;
    &lt;/&gt;
  );
};
 
export default Buttons;
</code></pre><h2 id="--3"><strong>总结</strong></h2><p>我们有很多方法可以使用过滤器组件来减少用户在我们的应用程序中搜索理想结果所浪费的时间。</p><p>在这个应用程序中，我们使用的数组中只有 10 个对象，但很多时候我们从 API 获得数据，那里可能有大量数据。在这种情况下，只进行一次搜索往往不能给出准确的结果，所以我们使用过滤器。</p><p>你可以在 <a href="https://github.com/Ateevduggal/Filter-Menu-in-React">GitHub Repo</a> 中看到全部代码。你可以通过应用程序的<a href="https://filter-menu-in-react.vercel.app/">链接</a>来检查这些过滤器按钮是如何运行的。</p><p>你也可以去看看我的其他项目：</p><ul><li><a href="https://tekolio.com/how-to-make-custom-pagination-in-react-js-with-hooks/">如何在 React 中使用 hooks 制作一个分页组件</a></li><li><a href="https://tekolio.com/how-to-create-a-dictionary-app-in-react/">如何在 React 中使用 hooks 制作一个字典应用程序</a></li><li><a href="https://tekolio.com/how-to-host-a-react-app-on-github-pages-with-a-custom-domain/">如何在 GitHub Pages 上托管一个有自定义域名 React 应用</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 更好的 React 性能——何时使用 useCallback 和 useMemo Hook ]]>
                </title>
                <description>
                    <![CDATA[ 我们都希望构建强大的应用，避免不必要的渲染。有一些钩子可以帮助你实现这个愿望，但你可能不确定钩子的选择和使用时机。 我们将通过本文学习 useCallback  和 useMemo的区别，以及如何衡量在代码中使用它们的收益。 在我们开始之前，请注意以下用于优化 React 的方法实际上是不得已的选择。代码本身可能有许多改进空间，在改进代码前，本文性能提升技巧可能还派不上用场。 但了解这些工具，以及知道如何使用它们很有必要。 帮助你理解文章的资料  * useCallback [https://beta.reactjs.org/apis/react/useCallback]  和 useMemo    [https://beta.reactjs.org/apis/react/useMemo]  的 Beta 版本官方文档  * 示例项目源码 [https://github.com/dastasoft/optimizing-react]  * 示例项目 Demo [https://react-optimisation.dastasoft.com/] 与往常一样，我提供了一个示例项 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/better-react-performance-usecallback-vs-usememo/</link>
                <guid isPermaLink="false">63a5771cf490ad0743626777</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Fri, 23 Dec 2022 06:38:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/12/useCallback-vs-useMemo.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/better-react-performance-usecallback-vs-usememo/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Better React Performance – When to Use the useCallback vs useMemo Hook</a>
      </p><!--kg-card-begin: markdown--><p>我们都希望构建强大的应用，避免不必要的渲染。有一些钩子可以帮助你实现这个愿望，但你可能不确定钩子的选择和使用时机。</p>
<p>我们将通过本文学习 <code>useCallback</code> 和 <code>useMemo</code>的区别，以及如何衡量在代码中使用它们的收益。</p>
<p>在我们开始之前，请注意以下用于优化 React 的方法实际上是不得已的选择。代码本身可能有许多改进空间，在改进代码前，本文性能提升技巧可能还派不上用场。</p>
<p>但了解这些工具，以及知道如何使用它们很有必要。</p>
<h2 id="">帮助你理解文章的资料</h2>
<ul>
<li><a href="https://beta.reactjs.org/apis/react/useCallback">useCallback</a> 和 <a href="https://beta.reactjs.org/apis/react/useMemo">useMemo</a> 的 Beta 版本官方文档</li>
<li><a href="https://github.com/dastasoft/optimizing-react">示例项目源码</a></li>
<li><a href="https://react-optimisation.dastasoft.com/">示例项目 Demo</a></li>
</ul>
<p>与往常一样，我提供了一个示例项目，以便你在简化的环境中测试本文说明的所有内容。示例项目是对你将要学习要点的总结。</p>
<p>在开始比较这两个钩子之前，让我们回顾一些必要的背景概念。</p>
<h2 id="referentialequality">什么是引用相等（Referential Equality）</h2>
<p>当 React 对比 <code>useEffect</code>、 <code>useCallback</code>的依赖数组的值，或者传入子组件的 props 值时，使用的是 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is"><code>Object.is()</code></a>。</p>
<p>详细介绍可以查看 <a href="http://Object.is">Object.is</a>，简言之：</p>
<ul>
<li>原始值是相等的（上文链接有少数例外）。</li>
<li>非原始值指向内存中相同的对象。</li>
</ul>
<blockquote>
<p>译者注：原始值指的是数据类型为基本数据类型（如：number、string、boolean 等）时，两个值相等的数据在严格模式下（<code>===</code>）也是相等的。非原始值值的数据类型是引用类型（如：object），由于引用类型存储的是对象的引用，所以只有当两个对象引用相同的底层对象，它们在严格模式下才是相等的。这种比较方式被称为“引用相等”。</p>
</blockquote>
<p>简化示例如下：</p>
<pre><code class="language-js">"string" === "string" // true
0 === 0 // true
true === true // true
{} === {} // false
[] === [] // false

const f = () =&gt; 'Hi'
const f1 = f
const f2 = f

f1 === f1 // true
f1 === f2 // true
</code></pre>
<h2 id="reactmemo">React.memo 的运行机制</h2>
<p>我将简单说明一下<code>React.memo</code>的运行机制（后文也会讲解）。你可以在合适的时候使用它来提升性能。</p>
<p>当想要避免子组件不必要的重新渲染（即便父组件发生了更改），你可以使用 <code>React.memo</code> 打包子组件 – 只要 props 不发生改变，就不会重复渲染。<strong>请注意此处是引用相等</strong>（译者注：沿用了旧版本 React 的<a href="https://reactjs.org/docs/shallow-compare.html">“浅比较”</a>）——子组件不会被重新渲染。</p>
<pre><code class="language-javascript">import { memo } from 'react';

const ChildComponent = (props) =&gt; {
  // ...
};

export default memo(ChildComponent);
</code></pre>
<p>现在你知道 <code>React.memo</code> 的运行机制，让我们开始应用吧。</p>
<h2 id="usecallback">useCallback 的运行机制</h2>
<p><code>useCallback</code> 是 React 用来优化代码的内置钩子之一。但正如你将看到的那样，它并不是直接为性能提升设计的钩子。</p>
<p>简单来说，<code>useCallback</code> 允许你在组件渲染之间保存 <em>函数定义</em>。</p>
<pre><code class="language-js">import { useCallback } from 'react';

const params = useCallback(() =&gt; {
  // ...
  return breed;
}, [breed]);
</code></pre>
<p>使用方法很简单：</p>
<ul>
<li>从 React 引入<code>useCallback</code>，因为它是内置钩子。</li>
<li>打包你想要保存定义的函数。</li>
<li>像使用 <code>useEffect</code>一样，传入依赖数组，告诉 React 这些存储的值（在这里是函数定义）何时更新。</li>
</ul>
<p>需要注意的是 <em>函数定义</em> 部分。它存储定义，而不是执行本身，也不是结果——所以每次调用时都会执行该函数。因此，不要使用这个钩子避免冗长的计算。</p>
<p>那么保存函数定义的好处在哪儿呢？</p>
<h3 id="">回到引用相等</h3>
<p>如果使用的是函数本身，而不是返回值，那么在：</p>
<ul>
<li><code>useEffect</code> 等钩子的依赖数组</li>
<li>子组件的 prop、上下文等</li>
</ul>
<p>要实现渲染之间真正的相等，<code>useCallback</code>就得保存<strong>内存中对同一个对象的的引用</strong>。</p>
<p>如果不使用这个钩子，每一次渲染函数都会重新指向内存中的另一个引用。即便使用<code>React.memo</code>打包子组件，React 也会认为是不同的函数。</p>
<p>你可以通过示例项目测试这个行为。在没有优化的版本中，每一次在输入框填写内容都会引发子组件的副作用。</p>
<p>在示例中，没有优化的版本只会导致一个虚拟的渲染放缓和重新抓取图片。但假设在一个大型的项目中，会导致客户端执行大量计算，或者服务器的巨大开销。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/use-callback-referential-equality.png" alt="use-callback-referential-equality" width="600" height="400" loading="lazy"></p>
<h2 id="usememo"><code>useMemo</code> 是如何运作的</h2>
<p>这是今天的第二个内置钩子。你可以把这个钩子当作直接优化的手段，因为它存储函数的结果，除非依赖数组发生变化，函数不会再次执行。</p>
<p>由于它可以存储函数的结果，防止在组件渲染之间重复执行，因此你可以在两种情况下使用此钩子。</p>
<h3 id="">引用相等</h3>
<p>和 <code>useCallback</code> 一样，我们也可以通过 <code>useMemo</code> 来实现引用相等——但这次是结果的相等。</p>
<p>如果函数的返回值类型在渲染间会被当作不同的值对待，如对象或者数组，你可以使用 <code>useMemo</code> 来实现引用相等。</p>
<pre><code class="language-js">import { useMemo } from 'react';

const params = useMemo(() =&gt; {
  // ...
  return { breed };
}, [breed]);
</code></pre>
<p>从上面例子我们可以得出这样使用 <code>useMemo</code>：</p>
<ul>
<li>由 React 引入 <code>useMemo</code>，因为它是内置钩子。</li>
<li>打包你想要保存结果的函数。</li>
<li>像使用 <code>useEffect</code> 一样，传入依赖数组，告诉 React 这些存储的值（在这里是函数的返回值）何时更新。</li>
</ul>
<p>在示例中，函数返回一个对象。 通过 <a href="http://Object.is">Object.is</a> 我们得知对象是不相等的，因为它们存储了不同的内存地址。但是<code>useMemo</code>可以保存相同的引用。</p>
<p>你可以像之前一样在示例项目中测试这个行为。在未优化版本中，每一次按下键盘，都会重新检索图片。使用 <code>useMemo</code>后，相等的返回值被保持，子组件不在重新检索图片。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/use-memo-referential-equality.png" alt="use-memo-referential-equality" width="600" height="400" loading="lazy"></p>
<h3 id="">昂贵的计算</h3>
<p>由于使用 <code>useMemo</code>保存了值，避免函数重复执行，所以我们可以使用它避免不必要的昂贵计算，提高网站的性能。</p>
<p>让我们查看示例项目：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/use-memo-expensive-calculation.png" alt="use-memo-expensive-calculation" width="600" height="400" loading="lazy"></p>
<p>有一个组件，给定一个数字 n，就会打印出第 n 个斐波那契数。但是算法采用的递归版本性能很差。</p>
<p>你会发现有一个常量不断被重复渲染。示例中的性能标尺（Performance Gauge）会改变 state（每秒添加或者删除方块 60 次）。由于 state 一直发生改变，所以计算斐波那契数的函数也在重复执行，即便给定的数字是一样的。</p>
<p>在这种情况下，当你在非优化版本中使用更大的数字，就会发现性能肉眼可见的下降。优化版本只会在你更改滑块中的数字（更改给定数字）时出现性能峰值，但其余渲染将跳过计算并直接提供结果。</p>
<p>这里的问题是，在我们的日常工作中，不会遇到可以称为“昂贵计算”的计算，使用 <code>useMemo</code> 的决定不一定是“总是”或“从不”。</p>
<h2 id="">何时优化</h2>
<p>到目前为止，你已经了解了通过一些指标确定何时使用不同的钩子来避免不必要的渲染和/或副作用。现在让我们定义一些通用规则来决定在那些不太清楚的情况下到底是否使用这些钩子：</p>
<ul>
<li>回顾你的代码，重新思考代码构建。你会发现最能提升性能的其实是你代码本身。更多信息可以查看 <a href="https://overreacted.io/before-you-memo/">Dan Abramov 的这篇博文</a>。</li>
<li>如果不能证明优化可以带来好处，就不要优化——优化也有成本。</li>
<li>如果你不希望做额外的工作来证明优化可以带来好处，那请诚实对待自己的内心：其实你也不想优化。</li>
</ul>
<h2 id="">如何衡量性能影响/收益</h2>
<p>最重要的优化规则（总是在检查代码之后再使用）是能够衡量更改是否生效以及增益百分比是多少。你这样做不仅是为了可以在下一次绩效评估提高相应的百分比。</p>
<p>当你怀疑存在性能问题或只是想检查代码可以改进部分时，有以下两种选择：</p>
<h3 id="">笨拙的方法</h3>
<p>我把这个方法也纳入到文章中来，是因为让我们面对现实吧：你一直在到处使用 <code>console.log</code> 调试代码，不是吗？别担心，我和你一样。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/crappy-debugger-meme.png" alt="crappy-debugger-meme" width="600" height="400" loading="lazy"></p>
<p>尝试衡量性能问题的一种快速方法是找出执行某个动作需要多长时间以及该动作执行了多少次。因此，可以这么做：</p>
<pre><code class="language-js">const t0 = performance.now();
expensiveCalculation(targetNumber);
const t1 = performance.now();
console.log(`Call to expensiveCalculation took ${t1 - t0} milliseconds.`);
console.count('Expensive Calculation');
</code></pre>
<p>但这种方法只能检测出一些你已经怀疑的非常明显的情况。</p>
<p>同时请小心 <code>StrictMode</code>，出于稳定性考虑，它可能会导致 <code>console.count</code> 重复渲染。</p>
<p>现在让我们查看正确的方法。</p>
<h3 id="">专业的方法</h3>
<p>在这个方法中，你将使用官方的 <a href="https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi">React 开发者工具</a>来检查代码片段的性能。一旦你在浏览器添加了这个扩展程序，你就可以打开浏览器，搜索 <code>Profiler</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/profiler.png" alt="profiler" width="600" height="400" loading="lazy"></p>
<p>我将通过示例项目演示，你也可以在你自己的项目中测试。</p>
<p>当你点击 <code>record</code> 按钮，然后开始进行你认为需要关注性能的一些行为，profiler 就会保存并且打印出这个过程具体发生的细节和解释。</p>
<p>如在昂贵计算项目中，我们对比的没有优化和 <code>useMemo</code> 版本的结果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/profiler-graph.png" alt="profiler-graph" width="600" height="400" loading="lazy"></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/profiler-graph-detailed.png" alt="profiler-graph-detailed" width="600" height="400" loading="lazy"></p>
<p>在两个版本中，我分别点击 record 按钮，等待几秒钟，再次点击 record 按钮获取结果。如你所见，在我们准备的极端案例中，可以见到巨大的性能提升。</p>
<p>让我们仔细观察 profiler 中发生了什么：</p>
<ul>
<li>灰色条目是在渲染间没有发生变化的组件，所以不用担心性能方面的问题。</li>
<li>绿色和黄色条目是发生变化的组件，你可以看到渲染需要多长时间。</li>
<li>如果你点击每一个条目，可以看到更多的解释信息和数据。</li>
</ul>
<p>我之后会出一篇文章详细介绍 profiler，但是现在让我们看几个使用小技巧：</p>
<ul>
<li>在 settings 图标 General 菜单下，勾选 <code>Highlight updates when components render.</code>。这将显示渲染的内容，并可以检测在某些操作下不被渲染的子组件。</li>
<li>在 settings 图标 Profiler 菜单下，勾选 <code>Record why each component rendered while profiling.</code> 这将对正在渲染的组件内容的添加简要说明，或许能帮助你找到哪里需要提升。</li>
</ul>
<h2 id="">总结</h2>
<p>如你所见，这两个常被误解的钩子是完全不同的函数，使用的场景也不太相同。现在你可以检查你现在或者过去的项目，来看看是否误用了这些钩子。</p>
<p>在未来 React 或许能够自动完成优化。但在撰写本文时，优化仍是一个应该谨慎对待并经过全面分析的过程。</p>
<p>我希望你觉得这篇教程有用，能帮助你使用 React 构建性能更好的应用程序。谢谢你阅读本文！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ React 中的关注点分离——如何使用容器组件和展示组件 ]]>
                </title>
                <description>
                    <![CDATA[ 许多 React 新手会将逻辑和展示代码放在同一个 React 组件中，不知道将两者分离的重要性，对于他们来说，最重要的是代码能运行。 但之后当他们需要对文件进行改动，就面临一项艰巨的任务。这个时候他们就得重新考虑将两者分离的问题。 产生问题的原因是他们不了解关注点分离的概念、展示组件和容器组件模型。所以我将在这篇文章讲解这方面内容，帮助你在项目开发早期缓解这个问题。 本文将深入探讨容器和展示组件，稍微讲解一下关注点分离。 话不多说，让我们开始吧！ 目录  * 什么是关注点分离  * 什么是容器组件和展示组件  * 为什么需要这两种组件  * 展示组件和容器组件示例  * 如何通过 React 钩子取代容器组件  * 总结 什么是关注点分离 关注点分离是一个在编程中广泛使用的概念。它指的是执行不同操作的逻辑不应被分组或结合在一起。 例如接下来的代码示例就违反了关注点分离。我们把获取数据和展示数据放在了同一个组件中。 若要解决这个问题，并且遵循关注点分离，我们应该将两块（即：获取数据和在 UI 上展示）逻辑分开放置在不同的组件。 此时就需要容器组件和展示组件模式。在下文 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/separation-of-concerns-react-container-and-presentational-components/</link>
                <guid isPermaLink="false">639877a1a7bffa07c7441604</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Tue, 13 Dec 2022 04:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/12/container-and-presentational-component-pattern-image.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/separation-of-concerns-react-container-and-presentational-components/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Separation of Concerns in React –How to Use Container and Presentational Components</a>
      </p><!--kg-card-begin: markdown--><p>许多 React 新手会将逻辑和展示代码放在同一个 React 组件中，不知道将两者分离的重要性，对于他们来说，最重要的是代码能运行。</p>
<p>但之后当他们需要对文件进行改动，就面临一项艰巨的任务。这个时候他们就得重新考虑将两者分离的问题。</p>
<p>产生问题的原因是他们不了解关注点分离的概念、展示组件和容器组件模型。所以我将在这篇文章讲解这方面内容，帮助你在项目开发早期缓解这个问题。</p>
<p>本文将深入探讨容器和展示组件，稍微讲解一下关注点分离。</p>
<p>话不多说，让我们开始吧！</p>
<h2 id="">目录</h2>
<ul>
<li><a href="#whatistheseparationofconcerns">什么是关注点分离</a></li>
<li><a href="#whatarecontainerandpresentationalcomponents">什么是容器组件和展示组件</a></li>
<li><a href="#whydoweneedthesecomponents">为什么需要这两种组件</a></li>
<li><a href="#presentationandcontainercomponentexample">展示组件和容器组件示例</a></li>
<li><a href="#howtoreplacecontainercomponentswithreacthooks">如何通过 React 钩子取代容器组件</a></li>
<li><a href="#summary">总结</a></li>
</ul>
<h2 id="whatistheseparationofconcerns">什么是关注点分离</h2>
<p>关注点分离是一个在编程中广泛使用的概念。它指的是执行不同操作的逻辑不应被分组或结合在一起。</p>
<p>例如接下来的代码示例就违反了关注点分离。我们把获取数据和展示数据放在了同一个组件中。</p>
<p>若要解决这个问题，并且遵循关注点分离，我们应该将两块（即：获取数据和在 UI 上展示）逻辑分开放置在不同的组件。</p>
<p>此时就需要容器组件和展示组件模式。在下文将做详细讲解。</p>
<h2 id="whatarecontainerandpresentationalcomponents">什么是容器组件和展示组件</h2>
<p>为了实现关注点分离我们需要两种类型的组件：</p>
<ul>
<li>容器组件</li>
<li>展示组件</li>
</ul>
<h3 id="">容器组件</h3>
<p>是提供、创建和持有数据并服务于子组件的组件。</p>
<p>容器组件的唯一工作是处理数据。它不包含自己的任何 UI。相反，展示组件作为使用这些数据的子组件。</p>
<p>一个简单的示例就是 <code>FetchUserContainer</code> 组件，包含获取所有用户的逻辑。</p>
<h3 id="">展示组件</h3>
<p>主要职责是在 UI 上呈现数据。从容器组件中获取数据。</p>
<p>这些组件是无状态的，除非它们需要状态来呈现 UI。它们不会更改收到的数据。</p>
<p>一个简单的示例就是 <code>UserList</code>组件，展示所有用户。</p>
<h2 id="whydoweneedthesecomponents">为什么需要这两种组件</h2>
<p>我们可以通过一个示例来理解，假设我们需要展示一份从 <a href="https://jsonplaceholder.typicode.com/">JSON placeholder API</a> 获取的帖子列表。代码如下：</p>
<pre><code class="language-typescript">import { useEffect, useState } from 'react';

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

/**
 * 我们不应该把逻辑和数据展示结合的例子
 */
export default function DisplayPosts() {
  const [posts, setPosts] = useState&lt;Post[] | null&gt;(null);
  const [isLoading, setIsLoading] = useState&lt;Boolean&gt;(false);
  const [error, setError] = useState&lt;unknown&gt;();

  // 逻辑
  useEffect(() =&gt; {
    (async () =&gt; {
      try {
        setIsLoading(true);
        const resp = await fetch('https://jsonplaceholder.typicode.com/posts');
        const data = await resp.json();
        setPosts(data);
        setIsLoading(false);
      } catch (err) {
        setError(err);
        setIsLoading(false);
      }
    })();
  }, []);

  //展示
  return isLoading ? (
    &lt;span&gt;Loading... &lt;/span&gt;
  ) : posts ? (
    &lt;ul&gt;
      {posts.map((post: Post) =&gt; (
        &lt;li key={`item-${post.id}`}&gt;
          &lt;span&gt;{post.title}&lt;/span&gt;
        &lt;/li&gt;
      ))}
    &lt;/ul&gt;
  ) : (
    &lt;span&gt;{JSON.stringify(error)}&lt;/span&gt;
  );
}
</code></pre>
<p>这个组件做了这些事：</p>
<ul>
<li>它包含三个变量： <code>posts</code>, <code>isLoading</code> 和 <code>error</code>。</li>
<li>使用 <code>useEffect</code> 来处理业务逻辑。 从 API 获取数据： <code>[https://jsonplaceholder.typicode.com/posts](https://jsonplaceholder.typicode.com/posts)</code>，获取方法采用的是 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch">fetch API</a>。</li>
<li>确保数据获取完毕后，使用 <code>setPosts</code>存储到 <code>posts</code>状态。</li>
<li>确保在不同的场景变换 <code>isLoading</code> 和 <code>error</code> 的值。</li>
<li>将这个逻辑放置在一个异步的 IIFE（立即调用函数）。</li>
<li>最后，我们以无序列表的形式返回帖子并映射我们获取的所有帖子。</li>
</ul>
<p>上面的问题是获取数据和显示数据的逻辑被编码到一个组件中。可以说组件现在与逻辑高耦合。这正是我们不想要的。</p>
<p>以下是我们需要容器和展示组件的原因：</p>
<ul>
<li>创建低耦合的组件</li>
<li>保持关注点分离</li>
<li>代码更易重构</li>
<li>代码更有组织性和可维护性</li>
<li>更易测试</li>
</ul>
<h2 id="presentationandcontainercomponentexample">展示组件和容器组件示例</h2>
<p>好了，讲解部分就到这里——让我们从一个简单的例子开始吧。我们将使用与上面相同的示例——从 JSON placeholder API 获取数据。</p>
<p>先理解文件结构：</p>
<ul>
<li>容器组件为 <code>PostContainer</code></li>
<li>有两个展示组件：
<ul>
<li><code>Posts</code>：展示无序列表</li>
<li><code>SinglePost</code>：呈现单个列表标签的组件。即呈现列表的每个元素。</li>
</ul>
</li>
</ul>
<p>注意：我们将把上述所有组件存储在一个名为 <code>components</code> 的单独文件夹中。</p>
<p>了解文件结构后，让我们从容器组件开始：<code>PostContainer</code>。将下面代码复制到 <code>components/PostContainer.tsx</code> 文件。</p>
<pre><code class="language-tsx">import { useEffect, useState } from 'react';
import { ISinglePost } from '../Definitions';
import Posts from './Posts';

export default function PostContainer() {
  const [posts, setPosts] = useState&lt;ISinglePost[] | null&gt;(null);
  const [isLoading, setIsLoading] = useState&lt;Boolean&gt;(false);
  const [error, setError] = useState&lt;unknown&gt;();

  useEffect(() =&gt; {
    (async () =&gt; {
      try {
        setIsLoading(true);
        const resp = await fetch('https://jsonplaceholder.typicode.com/posts');
        const data = await resp.json();
        setPosts(data.filter((post: ISinglePost) =&gt; post.userId === 1));
        setIsLoading(false);
      } catch (err) {
        setError(err);
        setIsLoading(false);
      }
    })();
  }, []);

  return isLoading ? (
    &lt;span&gt;Loading... &lt;/span&gt;
  ) : posts ? (
    &lt;Posts posts={posts} /&gt;
  ) : (
    &lt;span&gt;{JSON.stringify(error)}&lt;/span&gt;
  );
}
</code></pre>
<p>从本文上一节我们看到的例子来看，上面的代码只是包含了获取数据的逻辑。此逻辑存在于 <code>useEffect</code> 中。容器组件将数据传递给 <code>Posts</code> 展示组件。</p>
<p>让我们看一看 <code>Posts</code> 展示组件。将下面代码复制粘贴到 <code>components/Posts.tsx</code> 文件：</p>
<pre><code class="language-tsx">/**
 * 展示组件
 */

import { ISinglePost } from '../Definitions';
import SinglePost from './SinglePost';

export default function Posts(props: { posts: ISinglePost[] }) {
  return (
    &lt;ul
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center'
      }}
    &gt;
      {props.posts.map((post: ISinglePost) =&gt; (
        &lt;SinglePost {...post} /&gt;
      ))}
    &lt;/ul&gt;
  );
}
</code></pre>
<p>如你所见，这是一个简单的文件，包含一个 <code>ul</code> 标签——一个无序列表。该组件映射作为 props 传递的<code>posts</code>。然后传递给 <code>SinglePost</code> 组件。</p>
<p>还有另一个呈现列表标签的展示组件，即 <code>li</code> 标签。它显示帖子的标题和正文。将以下代码复制粘贴到 <code>components/SinglePost.tsx</code> 文件中：</p>
<pre><code class="language-tsx">import { ISinglePost } from '../Definitions';

export default function SinglePost(props: ISinglePost) {
  const { userId, id, title, body } = props;
  return (
    &lt;li key={`item-${userId}-${id}`} style={{ width: 400 }}&gt;
      &lt;h4&gt;
        &lt;strong&gt;{title}&lt;/strong&gt;
      &lt;/h4&gt;
      &lt;span&gt;{body}&lt;/span&gt;
    &lt;/li&gt;
  );
}
</code></pre>
<p>正如你所见，这些展示组件只是在屏幕上显示数据。就这样。其他什么都不做。由于它们用于显示数据，因此会有自己的样式。</p>
<p>我们已经设置好组件，让我们回顾一下做了些什么：</p>
<ul>
<li>在例子中没有违反关注点分离的概念。</li>
<li>为每个组件编写单元测试变得更加容易。</li>
<li>代码的可维护性和可读性要好得多。因此，我们的代码库变得更有条理。</li>
</ul>
<p>我们实现了我们想要的，但是我们利用钩子进一步增强这个模式。</p>
<h2 id="howtoreplacecontainercomponentswithreacthooks">如何通过 React 钩子取代容器组件</h2>
<p>自 <strong>React 16.8.0</strong> 以来，借助函数组件和钩子构建和开发组件变得更加容易。</p>
<p>我们将利用这一能力，用钩子替换容器组件。</p>
<p>将以下代码复制粘贴到 <code>hooks/usePosts.ts</code> 文件中：</p>
<pre><code class="language-tsx">import { useEffect, useState } from 'react';
import { ISinglePost } from '../Definitions';

export default function usePosts() {
  const [posts, setPosts] = useState&lt;ISinglePost[] | null&gt;(null);
  const [isLoading, setIsLoading] = useState&lt;Boolean&gt;(false);
  const [error, setError] = useState&lt;unknown&gt;();

  useEffect(() =&gt; {
    (async () =&gt; {
      try {
        setIsLoading(true);
        const resp = await fetch('https://jsonplaceholder.typicode.com/posts');
        const data = await resp.json();
        setPosts(data.filter((post: ISinglePost) =&gt; post.userId === 1));
        setIsLoading(false);
      } catch (err) {
        setError(err);
        setIsLoading(false);
      }
    })();
  }, []);

  return {
    isLoading,
    posts,
    error
  };
}
</code></pre>
<p>在这里：</p>
<ul>
<li>将 <code>PostContainer</code> 组件的逻辑提取到钩子中。</li>
<li>此钩子将返回一个包含 <code>isLoading</code>、<code>posts</code> 和<code>error</code>的对象。</li>
</ul>
<p>现在我们可以简单地移除容器组件 <code>PostContainer</code>。然后，我们可以直接在 <code>Posts</code> 展示组件中使用这个钩子，而不是将容器的数据作为 <code>prop</code> 传递给展示组件。</p>
<p>对<code>Post</code>组件进行以下编辑：</p>
<pre><code class="language-tsx">/**
 * 展示组件
 */

import { ISinglePost } from '../Definitions';
import usePosts from '../hooks/usePosts';
import SinglePost from './SinglePost';

export default function Posts(props: { posts: ISinglePost[] }) {
  const { isLoading, posts, error } = usePosts();

  return (
    &lt;ul
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center'
      }}
    &gt;
      {isLoading ? (
        &lt;span&gt;Loading...&lt;/span&gt;
      ) : posts ? (
        posts.map((post: ISinglePost) =&gt; &lt;SinglePost {...post} /&gt;)
      ) : (
        &lt;span&gt;{JSON.stringify(error)}&lt;/span&gt;
      )}
    &lt;/ul&gt;
  );
}
</code></pre>
<p>通过使用钩子，我们消除了存在于这些展示组件之上的额外组件层。</p>
<p>使用钩子，我们获得了与容器/展示组件模式相同的结果。</p>
<h2 id="summary">总结</h2>
<p>通过这篇文章，我们学习了：</p>
<ul>
<li>关注点分离</li>
<li>容器和展示组件</li>
<li>为什么需要这两种组件</li>
<li>钩子如何取代容器组件</li>
</ul>
<p>如果想要了解更多，我强烈推荐你阅读 <a href="https://tanstack.com/table/v8/">react-table:</a>。这个库使用了大量的钩子都是很好的示例。</p>
<p>你可以在 <a href="https://codesandbox.io/s/container-presentation-pattern-lm1osl?file=/src/components/PostContainer.tsx">codesandbox</a> 找到本文的完整代码。</p>
<p>感谢阅读！</p>
<p>可以在 <a href="https://twitter.com/keurplkar">Twitter</a>、<a href="https://github.com/keyurparalkar">GitHub</a>和 <a href="https://www.linkedin.com/in/keyur-paralkar-494415107/">LinkedIn</a> 上关注我。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 React 和 Dall-E 2 API 生成图像——React 和 OpenAI API 教程 ]]>
                </title>
                <description>
                    <![CDATA[ 嘿，大家好！OpenAI 刚刚发布了它的 DALL-E API，用户可以通过键入查询生成自定义图像。 在本教程中，你将学习如何集成 OpenAI DALL-E 2 API 与 React app。 但首先，Dell-E 是如何工作的呢 正如你已经知道的，你必须输入一个查询——就像 熊拿着画笔在星空里，文森特·梵高画。这里面有很多关键词，比如“画笔”、“星空”和“文森特·梵高”。 Dall-E 要做的是搜索这些与我上面提到的关键字相关的图像。然后它将使用人工智能将所有的图像合并为一个，然后提供给我们。 现在让我们学习如何将其集成到 React 应用程序中，以创建具有这些很棒的特性的应用程序。 如何创建 React 应用程序 现在，创建一个 React 应用程序。你可以使用 CRA（create-react-app）命令创建它，也可以使用 Vite。 我们需要一个文本字段和一个按钮作为 UI 组件。文本字段将用于从用户获取查询，而按钮将用于触发 API 请求。让我们同时创建一个状态来存储查询和一个函数，该函数将在单击按钮时运行。 import { useState } fro ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/generate-images-using-react-and-dall-e-api-react-and-openai-api-tutorial/</link>
                <guid isPermaLink="false">6391629a0bd1810766a86b0b</guid>
                
                    <category>
                        <![CDATA[ 人工智能 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Keren Ma ]]>
                </dc:creator>
                <pubDate>Thu, 08 Dec 2022 04:10:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/12/Important-Concepts-and-questions--1-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/generate-images-using-react-and-dall-e-api-react-and-openai-api-tutorial/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Generate Images using React and the Dall-E 2 API – React and OpenAI API Tutorial</a>
      </p><!--kg-card-begin: markdown--><p>嘿，大家好！OpenAI 刚刚发布了它的 DALL-E API，用户可以通过键入查询生成自定义图像。</p>
<p>在本教程中，你将学习如何集成 OpenAI DALL-E 2 API 与 React app。</p>
<h2 id="delle">但首先，Dell-E 是如何工作的呢</h2>
<p>正如你已经知道的，你必须输入一个查询——就像 <strong>熊拿着画笔在星空里，文森特·梵高画</strong>。这里面有很多关键词，比如“画笔”、“星空”和“文森特·梵高”。</p>
<p>Dall-E 要做的是搜索这些与我上面提到的关键字相关的图像。然后它将使用人工智能将所有的图像合并为一个，然后提供给我们。</p>
<p>现在让我们学习如何将其集成到 React 应用程序中，以创建具有这些很棒的特性的应用程序。</p>
<h2 id="react">如何创建 React 应用程序</h2>
<p>现在，创建一个 React 应用程序。你可以使用 CRA（create-react-app）命令创建它，也可以使用 Vite。</p>
<p>我们需要一个文本字段和一个按钮作为 UI 组件。文本字段将用于从用户获取查询，而按钮将用于触发 API 请求。让我们同时创建一个状态来存储查询和一个函数，该函数将在单击按钮时运行。</p>
<pre><code>import { useState } from "react";
import "./App.css";

function App() {
  const [prompt, setPrompt] = useState("");

  const generateImage = async () =&gt; {};

  return (
    &lt;div className="app-main"&gt;
      &lt;&gt;
        &lt;h2&gt;Generate an Image using Open AI API&lt;/h2&gt;

        &lt;textarea
          className="app-input"
          placeholder="Search Bears with Paint Brushes the Starry Night, painted by Vincent Van Gogh.."
          onChange={(e) =&gt; setPrompt(e.target.value)}
          rows="10"
          cols="40"
        /&gt;
        &lt;button onClick={generateImage}&gt;Generate an Image&lt;/button&gt;
      &lt;/&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>我们的输出将会像这样：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/11/Screenshot-2022-11-05-212826.png" alt="Screenshot-2022-11-05-212826" width="600" height="400" loading="lazy"></p>
<h3 id="dalle2apireact">如何将 DALL-E 2 API 集成到 React 应用程序</h3>
<p>让我们看看如何将 DALL-E 2 API 集成到我们的应用程序中。</p>
<p>首先，我们需要访问 <a href="https://beta.openai.com">OpenAI</a>网站。你需要注册以生成一个 API 密钥。你的账户里还会有 18 美元可以使用。</p>
<p>在注册时选择创建应用程序。</p>
<p>因此，在你创建了你的账户之后，转到 View API Keys 部分，在那里你可以创建你唯一的 API 密钥。查看下面的图片作为参考。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/11/Screenshot-2022-11-05-213523.png" alt="Screenshot-2022-11-05-213523" width="600" height="400" loading="lazy"></p>
<p>现在在 React App 中，创建一个 <strong>.env</strong> 文件。这是为了存储 API 密钥。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/11/Screenshot-2022-11-05-213733.png" alt="Screenshot-2022-11-05-213733" width="600" height="400" loading="lazy"></p>
<p>在这里添加 API 密钥。请注意，在 CRA 和 Vite React App 中，从 .env 文件中获取密钥的方法是不同的。所以请记住这一点。我使用的是 Vite，所以我们是这样做的：</p>
<pre><code>VITE_Open_AI_Key = "Your API Key"
</code></pre>
<p>现在已经添加了 API 密钥，我们需要在 App.js 或 App.jsx 文件中导入一些东西，包括来自 openai SDK 的 Configuration 和 OpenAIApi。但是，我们首先需要在 React App 安装 <strong>openai SDK</strong>。</p>
<p>只需输入下面的指令安装：</p>
<pre><code>npm install openai
</code></pre>
<p>安装可能需要一些时间。然后，像这样导入我们之前提到的两个东西：</p>
<pre><code>import { Configuration, OpenAIApi } from "openai";
</code></pre>
<p>我们需要创建一个配置变量，它将从 .env 文件中获取 API 密钥。</p>
<pre><code>const configuration = new Configuration({
	apiKey: import.meta.env.VITE_Open_AI_Key,
});
</code></pre>
<p>现在，我们需要将这个配置实例传递给 OpenAIApi，并为 OpenAIApi 创建一个新实例。</p>
<pre><code>const openai = new OpenAIApi(configuration);
</code></pre>
<p>以下是到目前为止的全部代码：</p>
<pre><code>import { Configuration, OpenAIApi } from "openai";

import { useState } from "react";
import "./App.css";

function App() {
  const [prompt, setPrompt] = useState("");
  const configuration = new Configuration({
    apiKey: import.meta.env.VITE_Open_AI_Key,
  });

  const openai = new OpenAIApi(configuration);
  

  return (
    &lt;div className="app-main"&gt;
      &lt;&gt;
        &lt;h2&gt;Generate an Image using Open AI API&lt;/h2&gt;

        &lt;textarea
          className="app-input"
          placeholder="Search Bears with Paint Brushes the Starry Night, painted by Vincent Van Gogh.."
          onChange={(e) =&gt; setPrompt(e.target.value)}
          rows="10"
          cols="40"
        /&gt;
        &lt;button onClick={generateImage}&gt;Generate an Image&lt;/button&gt;
      &lt;/&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>现在，在 <strong>generateImage</strong> 函数中，我们需要调用之前创建的 OpenAIApi 实例。记住，功能需求是异步的。</p>
<pre><code>const generateImage = async () =&gt; {
    await openai.createImage({
      prompt: prompt,
      n: 1,
      size: "512x512",
    });
  };
</code></pre>
<p>如你所见，我们使用的是 <strong>openai.createImage</strong>。这个 API 用于使用用户查询创建图像。它还需要<strong>数量 n</strong>，这是我们希望 API 返回的图像的数量，以及<strong>图像的尺寸 size</strong>。</p>
<p>有三种不同的图像尺寸，不同的价格，如下表所示。如果你使用的是 1024x1024 大小，每张图像将花费 0.020 美元。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/11/Screenshot-2022-11-05-215314.png" alt="Screenshot-2022-11-05-215314" width="600" height="400" loading="lazy"></p>
<p>现在这个 <strong>openai.createImage</strong> 返回一些可以存储在变量中的 response 接口。然后，我们可以从 response 变量获得生成的图像链接。</p>
<pre><code>const generateImage = async () =&gt; {
    const res = await openai.createImage({
      prompt: prompt,
      n: 1,
      size: "512x512",
    });

    console.log(res.data.data[0].url);
  };
</code></pre>
<p>但我们还是别这么做了。让我们再创建一个状态来存储这个图像链接，这样我们就可以在 UI 本身中查看图像。</p>
<pre><code>const [result, setResult] = useState("");

const generateImage = async () =&gt; {
    const res = await openai.createImage({
      prompt: prompt,
      n: 1,
      size: "512x512",
    });

    setResult(res.data.data[0].url);
  };
</code></pre>
<p>现在，图像链接将存储在 <strong>result</strong> 状态中。让我们在 UI 中渲染图像。但是由于结果最初是空的，我们可以创建一个查验。我们将只看到在状态中有链接的图像标记。</p>
<pre><code>{result.length &gt; 0 ? (
          &lt;img className="result-image" src={result} alt="result" /&gt;
        ) : (
          &lt;&gt;&lt;/&gt;
        )}
</code></pre>
<p>And here's the styling too:</p>
<pre><code>.result-image {
  margin-top: 20px;
  width: 350px;
}
</code></pre>
<p>UI 现在看起来像这样：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/11/Screenshot-2022-11-05-220108.png" alt="Screenshot-2022-11-05-220108" width="600" height="400" loading="lazy"></p>
<p>让我们输入一些东西并查看输出：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/11/Screenshot-2022-11-05-220222.png" alt="Screenshot-2022-11-05-220222" width="600" height="400" loading="lazy"></p>
<p>在上面的例子中，我输入了<strong>一匹在红色沙滩里的马</strong>，这是结果。</p>
<p>让我们尝试一些更复杂的东西，比如<strong>熊拿着画笔在星空里，文森特·梵高画</strong></p>
<p>结果如下：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/11/Screenshot-2022-11-05-220423.png" alt="Screenshot-2022-11-05-220423" width="600" height="400" loading="lazy"></p>
<p>以上就是你如何创建这个应用。你可以键入任何输入查询，它将通过人工智能生成该图像。</p>
<h2 id="">总结</h2>
<p>这就是所有的内容。现在你知道了如何使用 DALL-E 2 API 创建自己的 React 应用程序，以使用 AI 生成图像。你可以添加更多的功能，所以不妨尝试一下。</p>
<p>如果你想看视频版本，请点击我的视频<a href="https://youtu.be/oacBV4tnuYQ">使用 React 和 Dall-E API 生成图像——React 和 OpenAI API 教程</a> 在我的 YouTube 频道 <a href="https://www.youtube.com/c/CybernaticoByNishant">Cybernatico</a>。</p>
<p>查看代码 <a href="https://github.com/nishant-666/Dall-E-API-with-React">GitHub</a> 以供参考。</p>
<blockquote>
<p>学习快乐~</p>
</blockquote>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 通过代码示例详解 React State ]]>
                </title>
                <description>
                    <![CDATA[ state是React中最复杂的东西，初学者和有经验的开发人员都很难理解。因此，在本文中，我们将探索React中state的所有基础知识。 在理解state之前，让我们先了解一些基础知识，以便稍后更容易理解它。 如何用React在用户界面渲染数据 在屏幕上渲染任何东西，我们要使用ReactDOM.render  方法。 它的语法如下： ReactDOM.render(element, container[, callback])  * element  元素可以是任何HTML元素,JSX或返回JSX的组件  * container  容器是UI上我们想要在其中呈现数据的元素  * callback  回调是可选的函数，我们可以传递它，它在屏幕上渲染或重新渲染时被调用    看看下面的代码： import React from "react"; import ReactDOM from "react-dom"; const rootElement = document.getElementById("root"); ReactDOM.render(<h1>Welcome ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/what-is-state-in-react-explained-with-examples/</link>
                <guid isPermaLink="false">638db3ad832e3f0781763c0c</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Keren Ma ]]>
                </dc:creator>
                <pubDate>Mon, 05 Dec 2022 04:29:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/12/state.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/what-is-state-in-react-explained-with-examples/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How State Works in React – Explained with Code Examples</a>
      </p><!--kg-card-begin: markdown--><p>state是React中最复杂的东西，初学者和有经验的开发人员都很难理解。因此，在本文中，我们将探索React中state的所有基础知识。</p>
<p>在理解state之前，让我们先了解一些基础知识，以便稍后更容易理解它。</p>
<h2 id="react">如何用React在用户界面渲染数据</h2>
<p>在屏幕上渲染任何东西，我们要使用<code>ReactDOM.render</code> 方法。</p>
<p>它的语法如下：</p>
<pre><code class="language-plain">ReactDOM.render(element, container[, callback])
</code></pre>
<ul>
<li><code>element</code> 元素可以是任何HTML元素,JSX或返回JSX的组件</li>
<li><code>container</code> 容器是UI上我们想要在其中呈现数据的元素</li>
<li><code>callback</code> 回调是可选的函数，我们可以传递它，它在屏幕上渲染或重新渲染时被调用<br>
看看下面的代码：</li>
</ul>
<pre><code class="language-plain">import React from "react";
import ReactDOM from "react-dom";

const rootElement = document.getElementById("root");

ReactDOM.render(&lt;h1&gt;Welcome to React!&lt;/h1&gt;, rootElement);
</code></pre>
<p>这是一个<a href="https://codesandbox.io/s/focused-shockley-oh4tn?file=/src/index.js">代码示例</a>。</p>
<p>在这里，我们只是向屏幕渲染一个h1元素。</p>
<p>要渲染多个元素，可以这么写：</p>
<pre><code class="language-plain">import React from "react";
import ReactDOM from "react-dom";

const rootElement = document.getElementById("root");

ReactDOM.render(
  &lt;div&gt;
    &lt;h1&gt;Welcome to React!&lt;/h1&gt;
    &lt;p&gt;React is awesome.&lt;/p&gt;
  &lt;/div&gt;,
  rootElement
);
</code></pre>
<p>这是一个<a href="https://codesandbox.io/s/white-hooks-dgru0?file=/src/index.js">代码示例</a>。</p>
<p>如果内容变多，我们可以将JSX取出，放在一个变量中，这是渲染内容的首选方式，就像这样：</p>
<pre><code class="language-plain">import React from "react";
import ReactDOM from "react-dom";

const rootElement = document.getElementById("root");

const content = (
  &lt;div&gt;
    &lt;h1&gt;Welcome to React!&lt;/h1&gt;
    &lt;p&gt;React is awesome.&lt;/p&gt;
  &lt;/div&gt;
);

ReactDOM.render(content, rootElement);
</code></pre>
<p>这是一个<a href="https://codesandbox.io/s/trusting-night-5g825?file=/src/index.js">代码示例</a>。</p>
<p>在这里，我们还添加了一对额外的圆括号，以正确对齐JSX并使其成为单个JSX表达式。</p>
<p>如果你想详细了解JSX及其各种重要特性，请<a href="https://www.freecodecamp.org/news/jsx-in-react-introduction/">在这里查看我的文章</a>。</p>
<p>现在，让我们在屏幕上显示一个按钮和一些文本：</p>
<pre><code class="language-plain">import React from "react";
import ReactDOM from "react-dom";

const rootElement = document.getElementById("root");

let counter = 0;

const handleClick = () =&gt; {
  counter++;
  console.log("counter", counter);
};

const content = (
  &lt;div&gt;
    &lt;button onClick={handleClick}&gt;Increment counter&lt;/button&gt;
    &lt;div&gt;Counter value is {counter}&lt;/div&gt;
  &lt;/div&gt;
);

ReactDOM.render(content, rootElement);
</code></pre>
<p>这是一个<a href="https://codesandbox.io/s/quizzical-cohen-x55p8?file=/src/index.js">代码示例</a>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/counter_initial.gif" alt="counter_initial" width="600" height="400" loading="lazy"></p>
<p>正如你所看到的，当我们单击按钮时，计数器<code>counter</code> 的值会增加，就像您在控制台中看到的那样。但在UI上它没有更新。</p>
<p>这是因为我们在加载页面时，只使用 <code>ReactDOM.render</code>  方法一次，来渲染<code>counter</code> JSX的内容。我们不会再次调用它，所以即使<code>counter</code>的值在更新，它也不会显示在UI上。让我们来解决这个问题。</p>
<pre><code class="language-jsx">import React from "react";
import ReactDOM from "react-dom";

const rootElement = document.getElementById("root");

let counter = 0;

const handleClick = () =&gt; {
  counter++;
  console.log("counter", counter);
  renderContent();
};

const renderContent = () =&gt; {
  const content = (
    &lt;div&gt;
      &lt;button onClick={handleClick}&gt;Increment counter&lt;/button&gt;
      &lt;div&gt;Counter value is {counter}&lt;/div&gt;
    &lt;/div&gt;
  );

  ReactDOM.render(content, rootElement);
};

renderContent();
</code></pre>
<p>这是一个<a href="https://codesandbox.io/s/adoring-noether-8gsgu?file=/src/index.js">代码示例</a>。<br>
现在，我们在<code>renderContent</code> 函数内封装<code>content</code> JSX 和 <code>ReactDOM.render</code>方法。一旦它被定义好了，我们就调用函数在加载渲染用户界面上的内容。</p>
<p>注意，我们也在<code>renderContent</code> 函数内调用封装了<code>handleClick</code> 函数，所以每次我们点击按钮，<code>renderContent</code> 函数将被调用，界面内容也会更新。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/counter_updated.gif" alt="counter_updated" width="600" height="400" loading="lazy"></p>
<p>正如你所看到的，它正按照预期工作，计数器<code>counter</code> 的值正正确地显示在UI上。</p>
<p>你可能认为在每次单击按钮时重新呈现整个DOM的成本很高——但事实并非如此。这是因为React使用了Virtual DOM算法来检查UI上发生了什么变化，并且只重新呈现发生了变化的元素。因此，整个DOM不会再次被重新渲染。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/counter_preview.gif" alt="counter_preview" width="600" height="400" loading="lazy"></p>
<p>这是一个你可以自己尝试代码的<a href="https://8gsgu.csb.app/">效果预览</a>。</p>
<p>正如您在HTML结构中看到的，只有计数器 <code>counter</code>  的值被重新渲染，因为它是HTML结构中唯一显示的东西。这就是React如此之快的原因，虚拟DOM使React更加有用。</p>
<p>但是，每次我们想要更新UI时调用<code>renderContent</code> 函数仍然是不可行的。所以React添加了“state”的概念。</p>
<h2 id="reactstate">介绍React中的State</h2>
<p>状态允许我们管理应用程序中不断变化的数据。它被定义为一个对象，我们在其中定义键-值对，指定我们希望在应用程序中追踪的各种数据。</p>
<p>在React里，所有的代码在一个component组件里被定义。</p>
<p>在React中创建组件的方法主要有两种：</p>
<ul>
<li>类组件</li>
<li>函数组件</li>
</ul>
<blockquote>
<p>现在我们将从类组件开始。在本文后面，我们将看到一种创建函数组件的方法。<br>
你应该知道如何使用类组件以及函数组件，包括钩子。</p>
</blockquote>
<p>你不应该通过React钩子直接学习函数组件，而是应该首先理解类组件，这样才容易理解基础知识。</p>
<p>你可以通过使用ES6类关键字和扩展React提供的<code>Component</code>类来创建一个组件，如下所示：</p>
<pre><code class="language-jsx">import React from "react";
import ReactDOM from "react-dom";

class Counter extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    };

    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.state.counter = this.state.counter + 1;

    console.log("counter", this.state.counter);
  }

  render() {
    const { counter } = this.state;

    return (
      &lt;div&gt;
        &lt;button onClick={this.handleClick}&gt;Increment counter&lt;/button&gt;
        &lt;div&gt;Counter value is {counter}&lt;/div&gt;
      &lt;/div&gt;
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(&lt;Counter /&gt;, rootElement);
</code></pre>
<blockquote>
<p>注意组件的名称首字母要大写（<code>Counter</code>）。<br>
这是一个 <a href="https://codesandbox.io/s/nostalgic-burnell-57fhd?file=/src/index.js">代码示例</a>。</p>
</blockquote>
<p>看看这里我们在做什么。</p>
<ul>
<li>在构造函数内，首先通过传递props来调用 <code>super</code> 。</li>
<li>然后我们将状态定义为对象，将<code>counter</code> 定义为对象的属性。</li>
<li>我们还将<code>this</code>的环境绑定到<code>handleClick</code>函数因此在<code>handleClick</code>函数中我们得到了<code>this</code>的正确环境。</li>
<li>然后在<code>handleClick</code>函数中，我们更新<code>countor</code>并将其记录到控制台。</li>
<li>在<code>render</code>方法中，我们返回我们想要在UI上渲染的JSX。</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/counter_mutate_state.gif" alt="counter_mutate_state" width="600" height="400" loading="lazy"></p>
<p>正如您在控制台中所看到的，<code>counter</code>正在正确地更新——但它没有在UI上更新。</p>
<p>这是因为我们直接将 <code>handleClick</code>  函数中的状态更新为：</p>
<pre><code class="language-js">this.state.counter = this.state.counter + 1
</code></pre>
<p>因此React不会重新渲染组件（<strong>直接更新状态也是一个不好的做法</strong>）。</p>
<blockquote>
<p>永远不要在React中直接更新/改变状态，因为这是一个糟糕的做法，它会导致应用程序出现问题。另外，如果你直接更改状态，那么在状态更改时不会重新渲染组件。</p>
</blockquote>
<h2 id="setstate">setState的语法</h2>
<p>为了改变状态，React为我们提供了一个setState函数，允许我们更新状态的值。</p>
<p><code>setState</code>函数的语法如下：</p>
<pre><code>setState(updater, [callback])
</code></pre>
<ul>
<li><code>updater</code> 被更新的可以是函数或对象。</li>
<li><code>callback</code>回调函数是一个可选函数，在状态成功更新后执行。</li>
</ul>
<blockquote>
<p>调用 <code>setState</code> 会自动重新渲染整个组件及其所有子组件。我们不需要像前面使用<code>renderContent</code> 函数那样手动重新渲染。</p>
</blockquote>
<h2 id="react">如何使用函数更新React中的状态</h2>
<p>让我们修改 <a href="https://codesandbox.io/s/nostalgic-burnell-57fhd?file=/src/index.js">上面的代码示例</a> 来使用 <code>setState</code> 函数更新状态。</p>
<p>这是更新的 <a href="https://codesandbox.io/s/withered-dust-p3emg?file=/src/index.js">代码示例</a>。</p>
<p>如果你检查更新后的 <code>handleClick</code> 函数，它看起来像这样：</p>
<pre><code class="language-js">handleClick() {
  this.setState((prevState) =&gt; {
    return {
      counter: prevState.counter + 1
    };
  });

  console.log("counter", this.state.counter);
}
</code></pre>
<p>在这里，我们将一个函数作为 <code>setState</code> 方法的第一个参数传递，并返回一个新的状态对象，其中<code>counter</code> 在 <code>counter</code>的上一个值的基础上增加1。</p>
<p>我们在上面的代码中使用箭头函数，但是使用普通函数也可以。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/counter_updated_async.gif" alt="counter_updated_async" width="600" height="400" loading="lazy"></p>
<p>如果您注意到，我们正在正确地获得UI上 <code>counter</code> 的更新值。 但在控制台中,我们获取的是以前的 <code>counter</code> 值，尽管我们在调用 <code>this.setState</code> 之后添加了console.log。</p>
<blockquote>
<p>这是因为 <code>setState</code> 方法本质上是异步的。</p>
</blockquote>
<p>这意味着，尽管我们调用了<code>setState</code> 来一个个地增加 <code>counter</code> ，但它没有立刻生效。这是因为我们调用 <code>setState</code> 方法时，整个组件被重新渲染 – 因此React需要使用虚拟DOM算法检查需要更改的内容，然后执行各种检查以高效更新UI。</p>
<p>这是你可能没办法在调用 <code>setState</code>后，马上获得 <code>counter</code> 更新的原因。</p>
<blockquote>
<p>这在React中是非常重要的，因为你会在写代码时遇到调试困难的问题。请记住<code>setState</code> 在React里是异步的。</p>
</blockquote>
<p>如果您想在使用<code>setState</code> 后立即获得更新后的状态值， 你可以传递一个函数作为第二个参数，状态一更新就调用 <code>setState</code> 。</p>
<p>这是一个调整后的<a href="https://codesandbox.io/s/jolly-dawn-65wis?file=/src/index.js">代码示例</a>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/counter_updated_sync.gif" alt="counter_updated_sync" width="600" height="400" loading="lazy"></p>
<p>正如你所看到的，我们在控制台中和UI上同时获得了正确的 <code>counter</code> 值。</p>
<p>在上面的演示中，<code>handleClick</code>方法是这样的：</p>
<pre><code class="language-js">handleClick() {
  this.setState(
    (prevState) =&gt; {
      return {
        counter: prevState.counter + 1
      };
    },
    () =&gt; console.log("counter", this.state.counter)
  );
}
</code></pre>
<p>这里对于 <code>setState</code> 函数调用，我们传递了两个参数。第一个是返回新状态的函数，第二个是在状态更新后调用的回调函数。我们只是将更新的计数器值记录到回调函数中的控制台。</p>
<blockquote>
<p>尽管React提供了一个回调函数来立即获取更新后的状态值，但建议您只在快速测试或记录日志时使用它。</p>
</blockquote>
<p>相反，React建议你使用<code>componentDidUpdate</code>方法，这是一个React生命周期方法，看起来像这样：</p>
<pre><code class="language-js">componentDidUpdate(prevProps, prevState) {
  if (prevState.counter !== this.state.counter) {
    // do something
    console.log("counter", this.state.counter);
  }
}
</code></pre>
<p>这是一个 <a href="https://codesandbox.io/s/youthful-pine-txb1o?file=/src/index.js">代码示例</a>。</p>
<p>你可以在<a href="https://stackoverflow.com/questions/56501409/what-is-the-advantage-of-using-componentdidupdate-over-the-setstate-callback#answer-56502614">这里</a>找到更多关于为什么使用 <code>componentDidUpdate</code> 而不是回调 <code>setState</code>。</p>
<h2 id="">如何简化状态和方法声明</h2>
<p>如果你在上面的代码示例中看到构造函数代码，你会看到它看起来像这样：</p>
<pre><code class="language-js">constructor(props) {
  super(props);

  this.state = {
    counter: 0
  };

  this.handleClick = this.handleClick.bind(this);
}
</code></pre>
<p>要在 <code>handleClick</code> 事件处理程序中使用 <code>this</code> 关键字，我们必须像这样在构造函数中绑定它： <code>this:handleClick</code></p>
<pre><code class="language-js">this.handleClick = this.handleClick.bind(this);
</code></pre>
<p>此外，要声明状态，我们必须创建一个构造函数，在其中添加一个 <code>super</code> 调用，然后才能声明状态。<br>
这不仅繁琐，而且使代码变得不必要地复杂。</p>
<p>随着事件处理程序数量的增加，<code>.bind</code> 调用的数量也会增加。我们可以使用类属性语法来避免这样做。</p>
<p>这是一个更新的 <a href="https://codesandbox.io/s/sad-bassi-7fxnl?file=/src/index.js">代码示例</a> ，包含类属性语法。</p>
<p>在这里，我们像这样将状态直接移动到类内部：</p>
<pre><code class="language-js">state = {
   counter: 0
};
</code></pre>
<p>并且，<code>handlerClick</code> 事件处理器更改为箭头函数语法如下：</p>
<pre><code class="language-js">handleClick = () =&gt; {
  this.setState((prevState) =&gt; {
    return {
      counter: prevState.counter + 1
    };
  });
};
</code></pre>
<p>由于箭头函数没有自己的 <code>this</code> 上下文，所以它将使用上下文作为类，因此不需要使用 <code>.bind</code>  方法。</p>
<p>这使得代码更简单，更容易理解，因为我们不需要不停地绑定每个事件处理器。</p>
<blockquote>
<p><a href="https://github.com/facebook/create-react-app">create-react-app</a> 已经内置了对它的支持，现在就可以开始使用这个语法了。</p>
</blockquote>
<p>从现在开始，我们将使用这种语法，因为它是编写React组件的更流行和首选的方法。</p>
<p>如果您想了解更多关于类属性语法的知识，请查看 <a href="https://javascript.plainenglish.io/how-to-write-clean-and-easy-to-understand-react-code-using-class-properties-syntax-5b375b0618d3?source=friends_link&amp;sk=c170992cab9025fddb7b34b8894ea993">我的文章</a>。</p>
<h2 id="es">如何使用ES简化语法</h2>
<p>如果你在上面的代码示例中查看 <code>setState</code> 方法的调用，它会像这样：</p>
<pre><code class="language-js">this.setState((prevState) =&gt; {
  return {
    counter: prevState.counter + 1
  };
});
</code></pre>
<p>这么多代码，只是为了从一个函数中返回一个对象，我们用了5行代码。</p>
<p>我们能如下简化成一行：</p>
<pre><code class="language-js">this.setState((prevState) =&gt; ({ counter: prevState.counter + 1 }));
</code></pre>
<p>这里，我们将对象包装在圆括号中，使其隐式返回。这是可行的，因为如果我们在箭头函数中只有一条语句，我们可以跳过return关键字和花括号，像这样：</p>
<pre><code class="language-js">const add = (a, b) =&gt; { 
 return a + b;
}

// the above code is the same as below code:

const add = (a, b) =&gt; a + b;
</code></pre>
<p>但是由于左花括号被认为是函数体的开始，我们需要将对象包装在圆括号内，以使其正常工作。</p>
<p>这是一个更新的<a href="https://codesandbox.io/s/zen-galois-pew17?file=/src/index.js">代码示例</a>。</p>
<h2 id="react">如何在React中使用对象作为状态更新器</h2>
<p>在上面的代码中，我们使用一个函数作为<code>setState</code> 的第一个参数，但我们也可以传递一个对象作为参数。</p>
<p>这是一个<a href="https://codesandbox.io/s/zealous-nobel-yvvmw?file=/src/index.js">代码示例</a>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/updated_name.gif" alt="updated_name" width="600" height="400" loading="lazy"></p>
<p>组件代码如下所示：</p>
<pre><code class="language-js">class User extends React.Component {
  state = {
    name: "Mike"
  };

  handleChange = (event) =&gt; {
    const value = event.target.value;
    this.setState({ name: value });
  };

  render() {
    const { name } = this.state;

    return (
      &lt;div&gt;
        &lt;input
          type="text"
          onChange={this.handleChange}
          placeholder="Enter your name"
          value={name}
        /&gt;
        &lt;div&gt;Hello, {name}&lt;/div&gt;
      &lt;/div&gt;
    );
  }
}
</code></pre>
<p>在这里，我们添加了一个输入文本框，用户在其中输入自己的名字，当用户在文本框中输入时，它会显示在文本框的下方。</p>
<p>在状态中，我们初始化了name属性为 <code>Mike</code>  ，并在输入文本框中添加了一个 <code>onChange</code>  处理程序，如下所示：</p>
<pre><code class="language-js">state = {
  name: "Mike"
};

...

&lt;input
  type="text"
  onChange={this.handleChange}
  placeholder="Enter your name"
  value={name}
/&gt;
</code></pre>
<p>因此，当我们在文本框中键入任何内容时，我们通过向 <code>setState</code>  函数传递一个对象来更新输入值的状态。</p>
<pre><code class="language-js">handleChange = (event) =&gt; {
  const value = event.target.value;
  this.setState({ name: value });
}
</code></pre>
<blockquote>
<p>但是我们应该使用哪种形式的<code>setState</code>呢?我们必须决定是将一个对象还是一个函数作为第一个参数传递给<code>setState</code>函数。</p>
</blockquote>
<p>**答案是：**如果不需要 <code>prevState</code>  参数来查找下一个状态值，则传递一个对象。否则，将该函数作为第一个参数传递给 <code>setState</code>  。</p>
<p>但在传递对象作为参数时需要注意一个问题。</p>
<p>看看这个 <a href="https://codesandbox.io/s/eloquent-panini-u2ooe?file=/src/index.js">代码示例</a>.</p>
<p>在下面的代码中， <code>handleClick</code> 方法看起来像这样：</p>
<pre><code class="language-js">handleClick = () =&gt; {
  const { counter } = this.state;
  this.setState({
    counter: counter + 1
  });
}
</code></pre>
<p>我们取 <code>counter</code>  的当前值然后加1。它运行良好，如下图所示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/object_setstate_correct.gif" alt="object_setstate_correct" width="600" height="400" loading="lazy"></p>
<p>现在看看这个修改版本的 <a href="https://codesandbox.io/s/busy-johnson-oqvfn?file=/src/index.js">代码示例</a> 。</p>
<p>我们的 <code>handleClick</code> 方法现在看起来像这样：</p>
<pre><code class="language-js">handleClick = () =&gt; {
  this.setState({
    counter: 5
  });

  const { counter } = this.state;

  this.setState({
    counter: counter + 1
  });
}
</code></pre>
<p>在这里，我们首先将 <code>counter</code>  值设置为5，然后将其增加1。所以 <code>counter</code>  的预期值是6。我们来看看是不是这样。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/object_setstate_wrong.gif" alt="object_setstate_wrong" width="600" height="400" loading="lazy"></p>
<p>正如您所看到的，当我们第一次单击按钮时，我们期望 <code>counter</code>  值变成5，但它变成了1，并且在随后的每一次单击它都增加1。</p>
<p>这是因为，正如我们前面所看到的， <code>setState</code>  方法本质上是异步的。当我们调用 <code>setState</code>  时， <code>counter</code>   的值不会立即变为5，因此在下一行我们得到的 <code>counter</code>   值为0，这是我们在开始时初始化的状态。</p>
<p>所以当我们再次调用 <code>setState</code>  将 <code>counter</code>   加1时，它变成1，并且它只继续加1。</p>
<p>要解决这个问题，我们需要使用 <code>setState</code>  的更新程序语法，其中传递一个函数作为第一个参数。</p>
<p>这是一个<a href="https://codesandbox.io/s/strange-silence-qhykz?file=/src/index.js">代码示例</a>。</p>
<p>在上面的示例中， <code>handleClick</code>  方法看起来是这样的：</p>
<pre><code class="language-js">handleClick = () =&gt; {
  this.setState({
    counter: 5
  });

  this.setState((prevState) =&gt; {
    return {
      counter: prevState.counter + 1
    };
  });

  this.setState((prevState) =&gt; {
    return {
      counter: prevState.counter + 1
    };
  });
}
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/object_setstate_updater.gif" alt="object_setstate_updater" width="600" height="400" loading="lazy"></p>
<p>如您所见，当我们第一次单击按钮时， <code>counter</code>  的值变为7。这和预期的一样，因为首先我们把它设为5，然后把它加2次1，所以它就变成了7。即使我们多次点击按钮，它仍然保持在7，因为每次点击我们都将它重置为5并增加两次。</p>
<p>这是因为在 <code>handleClick</code>  内部，我们调用 <code>setState</code>  将计数器值设置为5，方法是将一个对象作为 <code>setState</code>  函数的第一个参数传递给该函数。在那之后，我们调用了两个 <code>setState</code>  调用，其中我们使用函数作为第一个参数。</p>
<p>那么这是如何正确工作的呢?</p>
<p>当React遇到 <code>setState</code>  调用时，它将调度更新以更改状态，因为它是异步的。但是在完成状态更改之前，React会看到有另一个 <code>setState</code> 调用。</p>
<p>因此，React不会立即使用新的 <code>counter</code>  值重新呈现。相反，它合并所有的 <code>setState</code>  调用，并基于前面 <code>counter</code> 的值更新 <code>counter</code> ，因为我们已经使用了 <code>prevState``.counter</code> 去计算 <code>counter</code>。</p>
<p>一旦所有 <code>setState</code>  调用都成功完成，React才重新渲染组件。因此，即使有三个 <code>setState</code>  调用，React也只会重新渲染组件一次，这可以通过在 <code>render</code>  方法中添加console.log语句来确认。</p>
<blockquote>
<p>因此，需要记住的一点是，在使用对象作为 <code>setState</code>  调用的第一个参数时应该小心，因为它可能会导致不可预知的结果。使用函数作为第一个参数，可以根据前面的结果获得正确的结果。</p>
</blockquote>
<p>你可能不会像我们在上面的演示中所做的那样一遍又一遍地调用 <code>setState</code>，但你可以在另一个函数中调用它，如下所示：</p>
<pre><code class="language-js">state = {
 isLoggedIn: false
};

...

doSomethingElse = () =&gt; {
 const { isLoggedIn } = this.state;
 if(isLoggedIn) {
   // do something different 
 }
};

handleClick = () =&gt; {
  // some code
  this.setState({ isLoggedIn: true);
  doSomethingElse();
}
</code></pre>
<p>在上面的代码中，我们已经定义了一个 <code>isLoggedIn</code>  状态，并且我们有两个函数 <code>handleClick</code>  和 <code>doSomethingElse</code> 。在 <code>handleClick</code>  函数中，我们将 <code>isLoggedIn</code>  状态值更新为<code>true</code>，并立即在下一行调用 <code>doSomethingElse</code>  函数。</p>
<p>在 <code>doSomethingElse</code>  中你可能认为你会得到 <code>isLoggedIn</code>  状态为<code>true if</code>条件中的代码会被执行。但是它不会被执行，因为 <code>setState</code>  是异步的，状态可能不会立即更新。</p>
<p>这就是为什么React添加了 <code>componendDidUpdate</code>  这样的生命周期方法，以便在状态或props更新时做一些事情。</p>
<blockquote>
<p>请注意检查您是否在下一行或下一个函数中再次使用相同的 <code>state</code>  变量，做些事来避免这些结果。</p>
</blockquote>
<h2 id="reactsetstate">如何合并React中的setState调用</h2>
<p>看看 <a href="https://codesandbox.io/s/bold-cache-zcj4u?file=/src/index.js">这个代码示例</a>。</p>
<p>这里，我们在状态中声明了 <code>username</code> 和 <code>counter</code> 属性，如下所示：</p>
<pre><code class="language-js">state = {
  counter: 0,
  username: ""
};
</code></pre>
<p>还如下声明了 <code>handleOnClick</code> 和 <code>handleOnChange</code> 事件处理器：</p>
<pre><code class="language-js">handleOnClick = () =&gt; {
  this.setState((prevState) =&gt; ({
    counter: prevState.counter + 1
  }));
};

handleOnChange = (event) =&gt; {
  this.setState({
    username: event.target.value
  });
};
</code></pre>
<p>检查下面函数中 <code>setState</code> 的调用。 你能看到在 <code>handleOnClick</code> 方法里，我们只设置了<code>counter</code>  的状态，在 <code>handleOnChange</code> 方法里只设置了<code>username</code> 的状态。</p>
<p>所以我们不需要像这样同时设置两个状态变量的状态：</p>
<pre><code class="language-js">this.setState((prevState) =&gt; ({
    counter: prevState.counter + 1,
    username: "somevalue"
}));
</code></pre>
<p>我们只能更新我们想要更新的那个。React会自动合并其他状态属性，所以我们不需要自己手动合并它们。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/state_merged-1.gif" alt="state_merged-1" width="600" height="400" loading="lazy"></p>
<p>正如您所看到的，我们成功地分别独立地更改了 <code>counter</code> 和 <code>username</code>。</p>
<h2 id="react">如何在React的函数组件中使用状态</h2>
<p>到目前为止，我们已经了解了如何在类组件中使用状态。现在让我们看看如何在函数组件中使用它。</p>
<p>函数组件与类组件类似，只是它们没有状态和生命周期方法。这就是为什么您可能听说过它们被称为无状态函数组件。</p>
<p>这些组件只接受props并返回一些JSX。</p>
<p>函数组件使代码更短，更容易理解和测试。</p>
<p>它们的执行速度也快一些，因为它们没有生命周期方法。在类组件中扩展的 <code>React.Component</code>  类 也没有为React带来的额外数据。</p>
<p>看看这个<a href="https://codesandbox.io/s/sleepy-pascal-8ugh3?file=/src/index.js">代码示例</a>。</p>
<p>在这里，我们从<a href="https://randomuser.me/">random user generator API</a>中加载一个包含20个随机用户的列表，当组件在 <code>componentDidMount</code>  方法中加载时，如下所示：</p>
<pre><code class="language-js">componentDidMount() {
  axios
    .get("https://randomuser.me/api/?page=0&amp;results=20")
    .then((response) =&gt; this.setState({ users: response.data.results }))
    .catch((error) =&gt; console.log(error));
}
</code></pre>
<p>一旦我们获取了那些用户，我们会设置它为 <code>users</code>  状态，并在UI上显示它。</p>
<pre><code class="language-jsx">{users.map((user) =&gt; (
  &lt;User key={user.login.uuid} name={user.name} email={user.email} /&gt;
))}
</code></pre>
<p>在这里，我们将需要显示的所有数据传递给 <code>User</code>  组件。</p>
<p><code>User</code>  组件像这样：</p>
<pre><code class="language-jsx">const User = (props) =&gt; {
  const { name, email } = props;
  const { first, last } = name;

  return (
    &lt;div&gt;
      &lt;p&gt;
        Name: {first} {last}
      &lt;/p&gt;
      &lt;p&gt;Email: {email} &lt;/p&gt;
      &lt;hr /&gt;
    &lt;/div&gt;
  );
};
</code></pre>
<p><strong>这个<code>User</code>  组件是一个函数组件。</strong></p>
<p>函数组件是以大写字母开头并返回JSX的函数。</p>
<p>无论组件是类组件还是函数组件，都要记住以像 <code>User</code>  这样的大写字母开头。这就是React在使用 <code>&lt;User /&gt;</code>等普通HTML元素时将其与普通HTML元素区别开来的原因。</p>
<p>如果我们使用 <code>&lt;user /&gt;</code> ，React将检查名称为user的HTML元素。因为没有这样的HTML元素，所以您不会得到想要的输出。</p>
<p>在上面的 <code>User</code> 函数组件中，我们将props传递给函数的props参数中的组件。</p>
<p>所以和在类组件中不用 <code>this.props</code> 一样，我们只使用 <code>props</code> 。</p>
<p>我们从不在函数组件中使用 <code>this</code> 关键字，因此避免了与此绑定相关的各种问题。</p>
<p>因此，函数组件优先于类组件。</p>
<p>一旦我们有了 <code>props</code> ，我们就会使用对象解构语法来获取其中的值并显示在UI上。</p>
<h2 id="reacthooks">如何在React Hooks中使用状态</h2>
<p>从版本16.8.0开始，React引入了钩子。它们完全改变了我们在React中编写代码的方式。使用React hook，我们可以在函数组件中使用状态和生命周期方法。</p>
<blockquote>
<p>React钩子是添加了状态和生命周期方法的函数组件。<br>
所以现在,类组件和函数组件之间没有什么区别。</p>
</blockquote>
<p>它们都可以有状态和生命周期方法。</p>
<p>但是，现在人们在写 React 组件时，React hook 更常用，因为它们使代码更短，更容易理解。</p>
<p>现在，你很少会发现使用类组件编写的 React 组件。</p>
<p>要使用React hook声明状态，我们需要使用 <code>useState</code>  钩子。</p>
<p><code>useState</code>  钩子接受一个参数，该参数是状态的初始值。</p>
<p>在类组件中，状态总是一个对象。但是在使用 <code>useState</code>   时，可以提供任何值作为初始值，如number, string, boolean, object, array, null等。</p>
<p><code>useState</code>  钩子返回一个数组，它的第一个值是当前状态的值。第二个值是我们将用于更新状态的函数，类似于 <code>setState</code>  方法。</p>
<p>让我们举一个使用状态的类组件的例子。我们将使用钩子将其转换为函数组件。</p>
<pre><code class="language-jsx">import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
  state = {
    counter: 0
  };

  handleOnClick = () =&gt; {
    this.setState(prevState =&gt; ({
      counter: prevState.counter + 1
    }));
  };

  render() {
    return (
      &lt;div&gt;
        &lt;p&gt;Counter value is: {this.state.counter} &lt;/p&gt;
        &lt;button onClick={this.handleOnClick}&gt;Increment&lt;/button&gt;
      &lt;/div&gt;
    );
  }
}

ReactDOM.render(&lt;App /&gt;, document.getElementById('root'));
</code></pre>
<p>这是使用类组件写的 <a href="https://codesandbox.io/s/delicate-thunder-xdpri?file=/src/index.js">代码示例</a> 。</p>
<p>让我们将上面的代码转换为使用钩子。</p>
<pre><code class="language-jsx">import React, { useState } from "react";
import ReactDOM from "react-dom";

const App = () =&gt; {
  const [counter, setCounter] = useState(0);

  return (
    &lt;div&gt;
      &lt;div&gt;
        &lt;p&gt;Counter value is: {counter} &lt;/p&gt;
        &lt;button onClick={() =&gt; setCounter(counter + 1)}&gt;Increment&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

ReactDOM.render(&lt;App /&gt;, document.getElementById("root"));
</code></pre>
<p>这是使用React钩子写的<a href="https://codesandbox.io/s/elegant-heyrovsky-3qco5?file=/src/index.js">代码示例</a>。</p>
<p>如您所见，使用React钩子使代码更简短，更容易理解。</p>
<p>让我们理解一下上面的代码：</p>
<ul>
<li>为了使用 <code>useState</code> 钩子，我们需要像第一行那样导入它。</li>
<li>在App组件内部，我们通过传递0作为初始值并使用解构语法来调用 <code>useState</code>  。我们将 <code>useState</code>  返回的数组值存储到 <code>counter</code>和 <code>setCounter</code>  变量中。</li>
<li>常用的约定是在用于更新状态的函数名前加上 <code>setCounter</code> 中的 <code>set</code>  关键字。</li>
<li>当单击递增按钮时，我们定义了一个内联函数，并通过传递更新的计数器值来调用 <code>setCounter</code>  函数。</li>
<li>注意，因为我们已经有了计数器的值，所以我们使用 <code>setCounter(counter + 1)</code> 来增加计数器的值。</li>
<li>由于内联on click处理程序中只有一条语句，所以不需要将代码移动到单独的函数中。不过，如果处理程序内部的代码变得复杂，您可以这样做。<br>
如果您想了解关于useState和其他React hook的更多细节(以及示例)，请查看我的React hook介绍文章。</li>
</ul>
<h3 id="thanksforreading">Thanks for reading!</h3>
<p>想要详细了解所有ES6+特性，包括let和const，promises、各种promise方法，数组和对象解构，箭头函数、async/await、导入和导出，以及更多？</p>
<p>请查阅我的《掌握现代JavaScript（<a href="https://modernjavascript.yogeshchavan.dev/">Mastering Modern JavaScript</a>）》一书。这本书涵盖了学习React的所有先决条件，并帮助您在JavaScript和React方面变得更好。</p>
<blockquote>
<p>在 <a href="https://www.freecodecamp.org/news/learn-modern-javascript/">这里</a> 查看这本书的免费预览内容。</p>
</blockquote>
<p>此外，您可以查看我的<strong>免费</strong><a href="https://yogeshchavan.podia.com/react-router-introduction">介绍React Router</a>课程，从零学习React Router。</p>
<p>想订阅 JavaScript, React, Node.js相关内容的日常更新？<a href="https://www.linkedin.com/in/yogesh-chavan97/">在领英关注我</a>。</p>
<p><a href="https://bit.ly/3w0DGum"><img src="https://gist.github.com/myogeshchavan97/98ae4f4ead57fde8d47fcf7641220b72/raw/c3e4265df4396d639a7938a83bffd570130483b1/banner.jpg" alt="banner" width="600" height="400" loading="lazy"></a></p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
