Skip to content

Latest commit

 

History

History
1028 lines (679 loc) · 37.8 KB

File metadata and controls

1028 lines (679 loc) · 37.8 KB

三、使用 React 挂钩编写第一个应用

在深入了解状态挂钩之后,我们现在将通过从头创建博客应用来利用它。在本章中,我们将学习如何以可伸缩的方式构造 React 应用,如何使用多个挂钩,在何处存储状态,以及如何使用挂钩解决常见用例。在本章的最后,我们将有一个基本的博客应用,在这里我们可以登录、注册和创建帖子。

本章将介绍以下主题:

  • 以可扩展的方式构建 React 项目
  • 从实体模型实现静态 React 组件
  • 用挂钩实现有状态组件

技术要求

应该已经安装了 Node.js 的最新版本(v11.12.0 或更高版本)。Node.js 的npm包管理器也需要安装。

本章的代码可以在 GitHub 存储库中找到:https://github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter03

请查看以下视频以查看代码的运行情况:

http://bit.ly/2Mm9yoC

Please note that it is highly recommended that you write the code on your own. Do not simply run the previously provided code examples. It is important that you write the code yourself in order to be able to learn and understand properly. However, if you run into any issues, you can always refer to the code example.

现在,让我们从这一章开始。

组织 React 项目

在学习了 React 的原理、如何使用useState挂钩以及挂钩如何在内部工作之后,我们现在将使用真正的useState挂钩来开发博客应用。首先,我们将创建一个新项目,并以一种允许我们以后扩展项目的方式构造文件夹。然后,我们将定义我们需要的组件,以涵盖博客应用的基本功能。最后,我们将使用挂钩向应用引入状态!在本章中,我们还将学习JSX以及ES6中引入的新 JavaScript 功能,直至ES2018

文件夹结构

项目的结构化方式有很多种,不同的结构可以很好地适用于不同的项目。通常,我们创建一个src/文件夹,并根据特征将文件分组。构建项目的另一种流行方式是按路线对项目进行分组。对于某些项目,可能需要另外按代码类型进行分隔,例如src/api/src/components/。然而,对于我们的项目,我们主要关注用户界面UI)。因此,我们将根据src/文件夹中的功能对文件进行分组。

It is a good idea to start with a simple structure at first, and only nest more deeply when you actually need it. Do not spend too much time thinking about the file structure when starting a project, because usually, you do not know up front how files should be grouped.

选择功能

我们首先要考虑我们将在博客应用中实现哪些功能。至少,我们希望实现以下功能:

  • 注册用户
  • 登录/注销
  • 查看单个帖子
  • 创建新帖子
  • 列名职位

既然我们已经选择了这些特性,那么让我们来设计一个初始的文件夹结构。

提出一个初始结构

从我们以前的功能中,我们可以抽象出几个功能组:

  • 用户(注册、登录/注销)
  • 发布(创建、查看、列出)

我们现在可以保持它非常简单,在src/文件夹中创建所有组件,而无需任何嵌套。然而,由于我们已经对博客应用需要的功能有了相当清晰的了解,我们现在可以提出一个简单的文件夹结构:

  • src/
  • src/user/
  • src/post/

在定义文件夹结构之后,我们可以转到组件结构。

组件结构

React 中组件的思想是让每个组件处理单个任务或 UI 元素。为了能够重用代码,我们应该尽力使组件尽可能细粒度。如果我们发现自己将代码从一个组件复制和粘贴到另一个组件,那么创建一个新组件并在多个其他组件中重用它可能是一个好主意。

通常,在开发软件时,我们从 UI 模型开始。对于我们的博客应用,模型如下所示:

Initial mock-up of our blog application

在拆分组件时,我们使用单一责任原则,即每个模块都应该对功能的单个封装部分负责。

在这个模型中,我们可以在每个组件和子组件周围画框,并给它们命名。请记住,每个组件都应该有一个职责。我们从构成此应用的基本组件开始:

Defining the fundamental components from our mock-up

我们为注销特性定义了一个Logout组件,一个CreatePost组件,其中包含创建新帖子的表单,以及一个Post组件来显示实际帖子。

现在我们已经定义了基本组件,我们将看看哪些组件在逻辑上属于一起,从而形成一个组。为此,我们现在定义容器组件,以便将组件组合在一起:

Defining the container components from our mock-up

我们定义了一个PostList组件来将帖子分组,然后定义了一个UserBar组件来处理登录/注销和注册。最后,我们定义了一个App组件,以便将所有内容组合在一起,并定义应用的结构。

现在我们已经完成了 React 项目的结构化,我们可以继续实现静态组件了。

实现静态组件

在开始通过挂钩向博客应用添加状态之前,我们将把应用的基本特性建模为静态组件。这样做意味着我们必须处理应用的静态视图结构。

首先处理静态结构是有意义的,这样可以避免以后必须将动态代码移动到不同的组件。此外,只处理**超文本标记语言(HTML)**和 CSS 更容易,这有助于我们快速开始项目。然后,我们可以继续实现动态代码和处理状态。

一步一步地做这件事,而不是一次完成所有的事情,有助于我们快速开始新的项目,而不必一次考虑太多,并让我们避免以后重新调整项目!

建立项目

我们已经学会了如何建立一个新的 React 项目。正如我们所了解的,我们可以使用create-react-app工具轻松地初始化一个新项目。我们现在要这样做:

  1. 首先,我们使用create-react-app初始化我们的项目:
>npx create-react-app chapter3_1
  1. 然后,我们为我们的功能创建文件夹:

现在我们的项目结构已经建立,我们可以开始实现组件了。

实现用户

我们将从静态组件的最简单特性开始:实现与用户相关的功能。正如我们从模型中看到的,我们需要四个组件:

  • 一个Login组件,我们将在用户尚未登录时显示该组件
  • 一个Register组件,我们还将在用户尚未登录时显示该组件
  • 一个Logout组件,用户登录后显示
  • 一个UserBar组件,它将有条件地显示其他组件

我们将首先定义前三个组件,它们都是独立的组件。最后,我们将定义UserBar组件,因为它取决于所定义的其他组件。

登录组件

首先,我们定义Login组件,其中显示两个字段:用户名字段和密码字段。此外,我们还显示了一个登录按钮:

  1. 我们首先为组件创建一个新文件:src/user/Login.js
  2. 在新创建的src/user/Login.js文件中,我们导入React
import React from 'react'
  1. 然后,我们定义我们的功能组件。目前,Login组件不接受任何道具:
export default function Login () {
  1. 最后,我们通过 JSX 返回两个字段和登录按钮。我们还定义了一个form容器元素来包装它们。为了避免表单提交时页面刷新,我们必须定义一个onSubmit处理程序并调用e.preventDefault()事件对象:
    return (
        <form onSubmit={e => e.preventDefault()}>
            <label htmlFor="login-username">Username:</label>
            <input type="text" name="login-username" id="login-username" />
            <label htmlFor="login-password">Password:</label>
            <input type="password" name="login-password" id="login-password" />
            <input type="submit" value="Login" />
        </form>
    )
}

这里,我们使用一个匿名函数来定义onSubmit处理程序。匿名函数的定义如下,如果它们没有任何参数:() => { ... },而不是function () { ... }。有了参数,我们可以写(arg1, arg2) => { ... },而不是function (arg1, arg2) { ... }。如果只有一个参数,我们可以省略()括号。此外,如果函数中只有一条语句,我们可以省略{}括号,如:e => e.preventDefault()

Using semantic HTML elements such as <form> and <label> make your app easier to navigate for people using accessibility assistance software, such as screen readers. Furthermore, when using semantic HTML, keyboard shortcuts, such as submitting forms by pressing the return key, automatically work.

我们的Login组件已经实现,现在可以测试了。

测试我们的组件

现在我们已经定义了第一个组件,让我们渲染它,看看它是什么样子:

  1. 首先,我们编辑src/App.js,并删除其所有内容。
  2. 然后,我们从导入ReactLogin组件开始:
import React from 'react'

import Login from './user/Login'

It is a good idea to group imports in blocks of code that belong together. In this case, we separate external imports, such as React, from local imports, such as our Login component, by adding an empty line in between. Doing so keeps our code readable, especially when we add more import statements later.

  1. 最后定义App组件,返回Login组件:
export default function App () {
    return <Login />
}

If we are only returning a single component, we can omit the brackets in the return statement. Instead of writing return (<Login />), we can simply write return <Login />.

  1. 在浏览器中打开http://localhost:3000,您应该会看到正在渲染的Login组件。如果已在浏览器中打开该页面,则在更改代码时,该页面应自动刷新:

The first component of our blog application: logging in by username and password

如我们所见,静态Login成分在 React 中呈现良好。我们现在可以进入Logout部分。

注销组件

接下来,我们定义Logout组件,该组件将显示当前登录的用户,以及一个用于注销的按钮:

  1. 新建文件:src/user/Logout.js
  2. 导入React,如下所示:
import React from 'react'
  1. 这一次,我们的函数将采用一个user道具,我们将使用它来显示当前登录的用户:
export default function Logout ({ user }) {

Here we use destructuring in order to extract the user key from the props object. React passes all component props, in a single object, as the first argument to a function. Using destructuring on the first argument is similar to doing const { user } = this.props in a class component.

  1. 最后,我们返回一个文本,显示当前登录的user和注销按钮:
    return (
        <form onSubmit={e => e.preventDefault()}>
            Logged in as: <b>{user}</b>
            <input type="submit" value="Logout" />
        </form>
    )
}
  1. 现在我们可以将Login组件替换为src/App.js中的Logout组件,以便查看我们新定义的组件(不要忘记将user道具传递给它!):
import React from 'react'

import Logout from './user/Logout'

export default function App () {
    return <Logout user="Daniel Bugl" />
}

现在,定义了Logout组件,我们可以继续讨论Register组件。

寄存器组件

静态Register组件与Login组件非常相似,有一个额外的字段来重复密码。如果它们如此相似,您可能会想到将它们合并到一个组件中,并添加一个道具来切换重复密码字段。但是,最好坚持单一责任原则,让每个组件只处理一个功能。稍后,我们将用动态代码扩展静态组件,然后RegisterLogin将有截然不同的代码。因此,我们需要在以后再次拆分它们。

尽管如此,让我们开始研究Register组件的代码:

  1. 我们首先创建一个新的src/user/Register.js文件,并从Login组件复制代码,毕竟静态组件非常相似。确保将组件名称更改为Register
import React from 'react'

export default function Register () {
    return (
        <form onSubmit={e => e.preventDefault()}>
            <label htmlFor="register-username">Username:</label>
            <input type="text" name="register-username" id="register-username" />
            <label htmlFor="register-password">Password:</label>
            <input type="password" name="register-password" id="register-password" />
  1. 接下来,我们在密码字段代码的正下方添加重复密码字段:
            <label htmlFor="register-password-repeat">Repeat password:</label>
            <input type="password" name="register-password-repeat" id="register-password-repeat" />
  1. 最后,我们还将 submit 按钮的值更改为 Register:
            <input type="submit" value="Register" />
        </form>
    )
}
  1. 同样,我们可以编辑src/App.js以显示我们的组件,方式与我们使用Login组件的方式类似:
import React from 'react'

import Register from './user/Register'

export default function App () {
    return <Register />
}

我们可以看到,我们的Register组件与Login组件非常相似。

用户栏组件

现在是时候把我们与用户相关的组件组合成一个UserBar组件了。这里我们将有条件地显示LoginRegister组件,或者Logout组件,这取决于用户是否已经登录。

让我们开始实现UserBar组件:

  1. 首先,我们创建一个新的src/user/UserBar.js文件,并导入React以及我们定义的三个组件:
import React from 'react'

import Login from './Login'
import Logout from './Logout'
import Register from './Register'
  1. 接下来,我们定义我们的函数组件,并为user定义一个值。现在,我们只需将其保存在静态变量中:
export default function UserBar () {
    const user = ''
  1. 然后,我们检查用户是否登录。如果用户登录,我们显示Logout组件,并将user值传递给它:
    if (user) {
        return <Logout user={user} />
  1. 否则,我们将显示LoginRegister组件。在这里,我们可以使用React.Fragment而不是<div>容器元素。这使我们的 UI 树保持干净,因为组件将简单地并排呈现,而不是包装在另一个元素中:
    } else {
        return (
            <React.Fragment>
                <Login />
                <Register />
            </React.Fragment>
        )
    }
}
  1. 再次编辑src/App.js,现在我们展示UserBar组件:
import React from 'react'

import UserBar from './user/UserBar'

export default function App () {
    return <UserBar />
}
  1. 正如我们所看到的,它是有效的!我们现在展示LoginRegister组件:

Our UserBar component, showing both the Login and Register components

  1. 接下来,我们可以编辑src/user/UserBar.js文件,并将user值设置为字符串:
        const user = 'Daniel Bugl' 
  1. 完成此操作后,我们的应用现在显示Logout组件:

Our app showing the Logout component after defining the user value

在本章后面,我们将向应用添加挂钩,这样我们就可以登录并动态更改状态,而无需编辑代码!

示例代码

用户相关组件的示例代码可在Chapter03/chapter3_1文件夹中找到。

只需运行npm install安装所有依赖项,并npm start启动应用,然后在浏览器中访问http://localhost:3000(如果它没有自动打开)。

执行职位

在实现了所有与用户相关的组件之后,我们继续在我们的博客应用中实现帖子。我们将定义以下组件:

  • 一个Post组件,用于显示单个 post
  • 用于创建新职位的CreatePost组件
  • 一个显示多个帖子的PostList组件

现在让我们开始实现与 post 相关的组件。

邮政部门

我们已经考虑过在创建实体模型时一篇文章包含哪些元素。文章应该有标题、内容和作者(写文章的用户)。

现在让我们实现Post组件:

  1. 首先,我们创建一个新文件:src/post/Post.js
  2. 然后我们导入React,定义我们的功能组件,接受三个道具:titlecontentauthor
import React from 'react'

export default function Post ({ title, content, author }) {
  1. 接下来,我们以类似实体模型的方式渲染所有道具:
    return (
        <div>
            <h3>{title}</h3>
            <div>{content}</div>
            <br />
            <i>Written by <b>{author}</b></i>
        </div>
    )
}
  1. 一如既往,我们可以通过编辑src/App.js文件来测试我们的组件:
import React from 'react'

import Post from './post/Post'

export default function App () {
    return <Post title="React Hooks" content="The greatest thing since sliced bread!" author="Daniel Bugl" />
}

现在,静态Post组件已经实现,我们可以继续讨论CreatePost组件。

CreatePost 组件

接下来,我们实现一个表单,以允许创建新的帖子。在这里,我们将user值作为道具传递给组件,因为作者应该始终是当前登录的用户。然后,我们展示作者,并为title提供一个输入字段,为博客文章的内容提供一个<textarea>元素。

现在让我们实现CreatePost组件:

  1. 新建文件:src/post/CreatePost.js
  2. 定义以下组件:
import React from 'react'

export default function CreatePost ({ user }) {
    return (
        <form onSubmit={e => e.preventDefault()}>
            <div>Author: <b>{user}</b></div>
            <div>
                <label htmlFor="create-title">Title:</label>
                <input type="text" name="create-title" id="create-title" />
            </div>
            <textarea />
            <input type="submit" value="Create" />
        </form>
    )
}
  1. 一如既往,我们可以通过编辑src/App.js文件来测试我们的组件:
import React from 'react'

import CreatePost from './post/CreatePost'

export default function App () {
    return <CreatePost />
}

正如我们所看到的,CreatePost组件呈现良好。我们现在可以进入PostList部分。

PostList 组件

在实现了其他与文章相关的组件之后,我们现在可以实现我们博客应用最重要的部分:博客文章的提要。现在,提要只是显示一个博客帖子列表。

现在让我们开始实现PostList组件:

  1. 我们首先导入ReactPost组件:
import React from 'react'

import Post from './Post'
  1. 然后,我们定义PostList函数组件,接受posts数组作为道具。如果posts未定义,则默认设置为空数组:
export default function PostList ({ posts = [] }) {
  1. 接下来,我们使用.map函数和 spread 语法呈现所有posts
    return (
        <div>
            {posts.map((p, i) => <Post {...p} key={'post-' + i} />)}
        </div>
    )
}

If we are rendering a list of elements, we have to give each element a unique key prop. React uses this key prop to efficiently compute the difference of two lists, when the data has changed.

这里,我们使用map函数,它将一个函数应用于数组的所有元素。这类似于使用for循环并存储所有结果,但它更简洁、声明性更强,更易于阅读!或者,我们可以执行以下操作,而不是使用map函数:

let renderedPosts = []
let i = 0
for (let p of posts) {
    renderedPosts.push(<Post {...p} key={'post-' + i} />)
    i++
}

return (
    <div>
        {renderedPosts}
    </div>
)

然后,我们返回每个 post 的<Post>组件,并将 post 对象p中的所有键作为道具传递给该组件。我们通过使用 spread 语法来实现这一点,其效果与手动将对象中的所有键作为道具列出相同,如下所示:<Post title={p.title} content={p.content} author={p.author} />

  1. 在模型中,我们在每一篇博文后都有一条水平线。我们可以通过使用React.Fragment在不增加<div>容器元素的情况下实现这一点:
{posts.map((p, i) => (
     <React.Fragment key={'post-' + i} >
          <Post {...p} />
          <hr />
     </React.Fragment>
))}

The key prop always has to be added to the uppermost parent element that is rendered within the map function. In this case, we had to move the key prop from the Post component to the React.Fragment component.

  1. 同样,我们通过编辑src/App.js文件来测试我们的组件:
import React from 'react'

import PostList from './post/PostList'

const posts = [
 { title: 'React Hooks', content: 'The greatest thing since sliced bread!', author: 'Daniel Bugl' },
 { title: 'Using React Fragments', content: 'Keeping the DOM tree clean!', author: 'Daniel Bugl' }
]

export default function App () {
    return <PostList posts={posts} />
}

现在,我们可以看到我们的应用列出了我们在posts数组中定义的所有帖子:

Showing multiple posts using the PostList component

正如我们所看到的,通过PostList组件列出多篇文章效果良好。我们现在可以继续将应用组装起来。

将应用组装在一起

在实现了所有组件之后,为了复制模型,我们现在只需要将所有东西都放在App组件中。然后,我们将成功地复制模型!

让我们开始修改App组件,并将我们的应用组装起来:

  1. 编辑src/App.js,删除所有当前代码。
  2. 首先,我们进口ReactPostListCreatePostUserBar组件:
import React from 'react'

import PostList from './post/PostList'
import CreatePost from './post/CreatePost'
import UserBar from './user/UserBar'
  1. 然后,我们为我们的应用定义一些模拟数据:
const user = 'Daniel Bugl'
const posts = [
    { title: 'React Hooks', content: 'The greatest thing since sliced bread!', author: 'Daniel Bugl' },
    { title: 'Using React Fragments', content: 'Keeping the DOM tree clean!', author: 'Daniel Bugl' }
]
  1. 接下来,我们定义App组件,并返回一个<div>容器元素,在其中设置一些填充:
export default function App () {
    return (
        <div style={{ padding: 8 }}>
  1. 现在,我们插入UserBarCreatePost组件,将user道具传递给CreatePost组件:
            <UserBar />
            <br />
            <CreatePost user={user} />
            <br />
            <hr />

Please note that you should always prefer spacing via CSS, rather than using the <br /> HTML tag. However, at the moment, we are focusing on the UI, rather than its style, so we simply use HTML whenever possible.

  1. 最后,我们显示PostList组件,列出所有posts
            <PostList posts={posts} />
        </div>
    )
}
  1. 保存文件后,http://localhost:3000会自动刷新,现在可以看到完整的界面:

Full implementation of our static blog app, according to the mock-up

正如我们所见,我们之前定义的所有静态组件都在一个App组件中呈现在一起。我们的应用现在看起来就像模型。接下来,我们可以继续使所有组件都成为动态的。

示例代码

我们博客应用静态实现的示例代码可以在Chapter03/chapter3_2文件夹中找到。

只需运行npm install安装所有依赖项和npm start启动应用,然后在浏览器中访问http://localhost:3000(如果它没有自动打开)。

用挂钩实现有状态组件

现在我们已经实现了应用的静态结构,我们将向它添加useState挂钩,以便能够处理状态和动态交互!

为用户特性添加挂钩

要为用户特性添加挂钩,我们必须用状态挂钩替换静态user值。然后,我们需要在登录、注册和注销时调整该值。

调整用户栏

回想一下,当我们创建UserBar组件时,我们静态地定义了user值。我们现在将用状态挂钩替换这个值!

让我们开始修改UserBar组件,使其具有动态性:

  1. 编辑src/user/UserBar.js,通过调整React导入语句导入useState挂钩,如下图:
import React, { useState } from 'react'
  1. 删除以下代码行:
    const user = 'Daniel Bugl'

将其替换为状态挂钩,使用空用户''作为默认值:

    const [ user, setUser ] = useState('')
  1. 然后,我们将setUser函数传递给LoginRegisterLogout组件:
    if (user) {
        return <Logout user={user} setUser={setUser} />
    } else {
        return (
            <React.Fragment>
                <Login setUser={setUser} />
                <Register setUser={setUser} />
            </React.Fragment>
        )
    }

现在,UserBar组件提供了一个setUser功能,可以在LoginRegisterLogout组件中使用该功能来设置或取消设置user值。

调整登录和注册组件

LoginRegister组件中,我们需要在登录或注册时使用setUser函数相应地设置user的值。

登录

Login组件中,我们暂时忽略密码字段,只处理用户名字段。

让我们从修改Login组件开始,以使其具有动态性:

  1. 编辑src/user/Login.js,导入useState挂钩:
import React, { useState } from 'react'
  1. 然后,调整函数定义以接受setUser道具:
export default function Login ({ setUser }) {
  1. 现在,我们为用户名字段的值定义一个新的状态挂钩:
    const [ username, setUsername ] = useState('')
  1. 接下来,我们定义一个处理函数:
    function handleUsername (evt) {
        setUsername(evt.target.value)
    }
  1. 然后调整input字段,使用username值,当输入改变时调用handleUsername函数:
            <input type="text" value={username} onChange={handleUsername} name="login-username" id="login-username" />
  1. 最后,我们需要在按下登录按钮时调用setUser函数,从而提交form
            <form onSubmit={e => { e.preventDefault(); setUser(username) }} />
  1. 此外,当username值为空时,我们可以禁用登录按钮:
            <input type="submit" value="Login" disabled={username.length === 0} />

现在我们可以输入用户名,按下登录按钮,然后我们的UserBar组件将改变其状态,并显示Logout组件!

登记

对于注册,我们还要检查输入的密码是否相同,然后才设置user值。

让我们从修改Register组件开始,以使其具有动态性:

  1. 首先,为了处理username字段,我们执行了与Login相同的步骤:
import React, { useState } from 'react'

export default function Register ({ setUser }) {
 const [ username, setUsername ] = useState('')

 function handleUsername (evt) {
 setUsername(evt.target.value)
 }

    return (
        <form onSubmit={e => { e.preventDefault(); setUser(username) }}>
            <label htmlFor="register-username">Username:</label>
            <input type="text" value={username} onChange={handleUsername} name="register-username" id="register-username" />
            <label htmlFor="register-password">Password:</label>
            <input type="password" name="register-password" id="register-password" />
            <label htmlFor="register-password-repeat">Repeat password:</label>
            <input type="password" name="register-password-repeat" id="register-password-repeat" />
            <input type="submit" value="Register" disabled={username.length === 0} />
        </form>
    )
}
  1. 现在,我们为密码和重复密码字段定义了两个新的状态挂钩:
    const [ password, setPassword ] = useState('')
    const [ passwordRepeat, setPasswordRepeat ] = useState('')
  1. 然后,我们为它们定义两个处理函数:
    function handlePassword (evt) {
        setPassword(evt.target.value)
    }

    function handlePasswordRepeat (evt) {
        setPasswordRepeat(evt.target.value)
    }

You might have noticed that we are always writing similar handler functions for input fields. Actually, this is the perfect use case for creating a custom Hook! We are going to learn how to do that in a future chapter.

  1. 接下来,我们将valueonChange处理函数分配给input字段:
             <label htmlFor="register-password">Password:</label>
             <input type="password" value={password} onChange={handlePassword} name="register-password" id="register-password" />
             <label htmlFor="register-password-repeat">Repeat password:</label>
             <input type="password" value={passwordRepeat} onChange={handlePasswordRepeat} name="register-password-repeat" id="register-password-repeat" />
  1. 最后,我们检查密码是否匹配,如果不匹配,我们保留按钮disabled
             <input type="submit" value="Register" disabled={username.length === 0 || password.length === 0 || password !== passwordRepeat} />

现在,我们已经成功地实现了密码是否相等的检查,并且实现了注册!

调整注销

用户功能仍然缺少一点,我们还不能注销。

现在让我们将Logout组件动态化:

  1. 编辑src/user/Logout.js,增加setUser道具:
export default function Logout ({ user, setUser }) {
  1. 然后,调整formonSubmit处理程序,并将用户设置为''
            <form onSubmit={e => { e.preventDefault(); setUser('') }} />

As we are not creating a new Hook here, we do not need to import the useState Hook from React. We can simply use the setUser function passed to the Logout component as a prop.

现在,当我们点击注销按钮时,Logout组件将user值设置为''

将用户传递到 CreatePost

正如您可能已经注意到的,CreatePost组件仍然使用硬编码的用户名。为了能够访问那里的user值,我们需要将挂钩从UserBar组件移动到App组件。

现在让我们重构user状态挂钩的定义:

  1. 编辑src/user/UserBar.js,并剪切/删除其中的挂钩定义:
    const [ user, setUser ] = useState('')
  1. 然后,我们编辑函数定义,并接受这两个值作为道具:
export default function UserBar ({ user, setUser }) {
  1. 现在我们编辑src/App.js,在那里导入useState挂钩:
import React, { useState } from 'react'
  1. 接下来,我们移除静态user值定义:
    const user = 'Daniel Bugl'
  1. 然后,我们将前面剪切的user状态挂钩插入App组件函数:
    const [ user, setUser ] = useState('')
  1. 现在我们可以将usersetUser作为道具传递给UserBar组件:
            <UserBar user={user} setUser={setUser} />

The user state is a global state, so we are going to need it in many components across the app. At the moment, this means that we need to pass down the user value and the setUser function to each component that needs it. In a future chapter, we are going to learn about React Context Hooks, which solve the problem of having to pass down props in such a way.

  1. 最后,我们仅在用户登录时显示CreatePost组件。为此,我们使用一个模式,它允许我们根据条件显示组件:
 {user && <CreatePost user={user} />}

现在,用户特性已经完全实现,我们可以使用LoginRegister组件,user值也可以传递到CreatePost组件!

为 posts 功能添加挂钩

在实现了用户特性之后,我们现在将实现动态创建帖子。我们首先调整App组件,然后修改CreatePost组件,以便能够插入新的帖子。

让我们从调整应用组件开始。

调整应用组件

正如我们从用户特性中了解到的,帖子也将是全局状态,因此我们应该在App组件中定义它。

现在让我们将posts值实现为全局状态:

  1. 编辑src/App.js,将当前posts数组重命名为defaultPosts
const defaultPosts = [
    { title: 'React Hooks', content: 'The greatest thing since sliced bread!', author: 'Daniel Bugl' },
    { title: 'Using React Fragments', content: 'Keeping the DOM tree clean!', author: 'Daniel Bugl' }
]
  1. 然后,为posts状态定义一个新的状态挂钩:
    const [ posts, setPosts ] = useState(defaultPosts)
  1. 现在,我们将posts值和setPosts函数作为道具传递给CreatePost组件:
            {user && <CreatePost user={user} posts={posts} setPosts={setPosts} />}

现在,我们的App组件提供posts数组,并为CreatePost组件提供setPosts函数。让我们继续调整 CreatePost 组件。

调整 CreatePost 组件

接下来,当我们按下创建按钮时,我们需要使用setPosts功能插入一篇新文章。

让我们开始修改CreatePost组件,使其具有动态性:

  1. 编辑src/posts/CreatePost.js,导入useState挂钩:
import React, { useState } from 'react'
  1. 然后,调整函数定义以接受postssetPosts道具:
export default function CreatePost ({ user, posts, setPosts }) {
  1. 接下来,我们定义两个新的状态挂钩,一个用于title值,另一个用于content值:
    const [ title, setTitle ] = useState('')
    const [ content, setContent ] = useState('')
  1. 现在,我们定义了两个处理函数,一个用于input字段,另一个用于textarea
    function handleTitle (evt) {
        setTitle(evt.target.value)
    }

    function handleContent (evt) {
        setContent(evt.target.value)
    }
  1. 我们还为“创建”按钮定义了一个处理程序函数:
    function handleCreate () {
  1. 在这个函数中,我们首先从input字段值创建一个newPost对象:
        const newPost = { title, content, author: user }

In newer JavaScript versions, we can shorten the following object assignment: { title: title }, to { title }, and it will have the same effect. So, instead of doing { title: title, contents: contents }, we can simply do { title, contents }.

  1. 然后,我们设置新的posts数组,首先在数组中添加newPost,然后使用扩展语法列出所有现有的posts
        setPosts([ newPost, ...posts ])
    }
  1. 接下来,我们将value和处理函数添加到input字段和textarea元素中:
             <div>
                 <label htmlFor="create-title">Title:</label>
                 <input type="text" value={title} onChange={handleTitle} name="create-title" 
                        id="create-title" />
             </div>
             <textarea value={content} onChange={handleContent} />

Usually in HTML, we put the value of textarea as its children. However, in React, textarea can be handled like any other input field, by using the value and onChange props.

  1. 最后,我们将handleCreate函数传递给form元素的onSubmit处理程序:
         <form onSubmit={e => { e.preventDefault(); handleCreate() }}>
  1. 现在,我们可以登录并创建一个新帖子,它将插入提要的开头:

Our first version of the blog app using Hooks, after inserting a new blog post

正如我们所看到的,现在我们的应用是完全动态的,我们可以使用它的所有功能!

示例代码

Chapter03/chapter3_3文件夹中可以找到我们的博客应用的动态实现的示例代码。

只需运行npm install安装所有依赖项,并npm start启动应用,然后在浏览器中访问http://localhost:3000(如果它没有自动打开)。

总结

在本章中,我们从头开发了自己的博客应用!我们从一个模型开始,然后创建静态组件来模拟它。之后,我们实现了挂钩,以允许动态行为。在本章中,我们学习了如何使用挂钩处理局部和全局状态。此外,我们还学习了如何使用多个挂钩,以及在哪些组件中定义挂钩和存储状态。我们还学习了如何解决常见用例,例如使用挂钩处理输入字段。

在下一章中,我们将学习useReducer挂钩,它允许我们更轻松地处理某些状态变化。此外,我们将学习useEffect挂钩,它允许我们运行带有副作用的代码。

问题

为了总结本章所学内容,请尝试回答以下问题:

  1. React 中文件夹结构的最佳实践是什么?
  2. 分解 React 组件时应使用哪一原则?
  3. map函数的作用是什么?
  4. 解构是如何工作的,我们什么时候使用它?
  5. spread 操作符是如何工作的,我们什么时候使用它?
  6. 我们如何使用 React 挂钩处理输入字段?
  7. 应该在哪里定义本地状态挂钩?
  8. 什么是全球状态?
  9. 全局状态挂钩应该定义在哪里?

进一步阅读

如果您对我们在本章中所学概念的更多信息感兴趣,请阅读以下阅读材料: