第 4 章 使用 Reducer 和 Effect Hook

第 4 章 使用 Reducer 和 Effect Hook

Flying
2021-01-28 / 0 评论 / 153 阅读 / 正在检测是否收录...

在使用 State Hook 开发我们自己的博客应用程序之后,我们现在将了解 React 提供的另外两个非常重要的 Hook:ReducerEffect Hook。我们首先要学习何时应该使用 Reducer Hook 而不是 State Hook。然后,我们将学习如何将现有的 State Hook 转换为 Reducer Hook,以便在实践中掌握该概念。接下来,我们将了解 Effect Hook 及其用途。最后,我们将在我们的博客应用程序中实现它们。

本章将介绍以下主题:

  • 了解 Reducer Hook 和 State Hook 之间的区别
  • 在我们的博客应用程序中实现 Reducer Hook
  • 在我们的博客应用程序中使用 Effect Hook

技术要求

应该已经安装了相当新版本的 Node.js(vll.l2.0 或更高版本)。还需要安装 Node.js 的 npm 包管理器。

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

观看以下视频,了解代码的实际应用:

http://bit.ly/2Mm9yoC

请注意,强烈建议您自己编写代码。不要简单地运行以前提供的代码示例。重要的是你自己编写代码,以便能够正确学习和理解。但是,如果遇到任何问题,始终可以参考代码示例。

现在,让我们从本章开始。

Reducer Hook 与 State Hook

在上一章中,我们学习了如何处理本地状态和全局状态。我们在这两种情况下都使用了 State Hook,这对于简单的状态更改来说很好。但是,当我们的状态逻辑变得更加复杂时,我们将需要确保保持状态一致。为此,我们应该使用 Reducer Hook 而不是多个 State Hook,因为很难在相互依赖的多个 State Hook 之间保持同步。作为替代方案,我们可以将所有状态保留在一个 State Hook 中,但随后我们必须确保我们不会意外覆盖状态的某些部分。

State Hook 的问题

State Hook 已经支持将复杂的对象和数组传递给它,并且可以很好地处理它们的状态变化。但是,我们总是必须直接更改状态,这意味着我们需要使用大量的扩展运算符语法,以确保我们不会覆盖状态的其他部分。例如,假设我们有一个这样的状态对象:

const [config, setConfig] = usestate({
  filter: 'all',
  expandPosts: true
})

现在,我们要更改 filter

setConfig({
  filter: {
    byAuthor: 'Daniel Bugl',
    fromDate: '2019-04-29'
  }
})

如果我们只是运行前面的代码,我们将删除先前状态的 expandPosts 部分!因此,我们需要执行以下操作:

setConfig({
  ...config,
  filter: {
    byAuthor: 'Daniel Bugl',
    fromDate: '2019-04-29'
  }
})

现在,如果我们想将 fromDate 过滤器更改为不同的日期,我们需要使用两次扩展运算符语法,以避免删除 byAuthor 过滤器:

setConfig({ 
  ...config, 
  filter: { 
   ...config.filter, 
   fromDate: '2019-04-30' 
  } 
})

但是,如果我们在 filter 状态仍然是字符串时执行此操作会发生什么?我们将得到以下结果:

{ 
  filter: { '0': 'a', '1': 'l', '2': 'l', fromDate: '2019-04-30' }, 
  expandPosts: true 
}

什么?为什么突然出现了三个新键——012?这是因为扩展运算符语法也适用于字符串,字符串的扩展的方式是每个字母根据其在字符串中的索引获得一个键。

可以想象,对于较大的状态对象,使用扩展语法并直接更改状态对象可能会变得非常乏味。此外,我们始终需要确保我们不会引入任何错误,并且我们需要检查整个应用程序中多个位置的错误。

Action

我们可以创建一个处理状态更改的函数,而不是直接更改状态。此类函数仅允许通过某些操作(例如 CHANGE_FILTERTOGGLE_EXPAND action)更改状态。

Action 只是具有类型键的对象,告诉我们正在处理哪个 action,以及更密切地描述 action 的其他键。

TOGGLE_EXPAND action 非常简单。它只是一个定义了操作 type 的对象:

{ type: 'TOGGLE_EXPAND' }

CHANGE_FILTER action 可以处理我们之前遇到的复杂状态更改,如下所示:

{ type: 'CHANGE_FILTER', all: true }
{ type: 'CHANGE_FILTER', fromDate: '2019-04-29' }
{ type: 'CHANGE_FILTER', byAuthor: 'Daniel Bugl' }
{ type: 'CHANGE_FILTER', fromDate: '2019-04-30' }

第二个、第三个和第四个 action 会将 filter 状态从字符串更改为对象,然后设置相应的键。如果对象已经存在,我们只需调整 action 中定义的键。每次调度后,状态将更改如下:

  • { expandPosts: true, filter: 'all' }
  • { expandPosts: true, filter: { fromDate: '2019-04-29' } }
  • { expandPosts: true, filter: { fromDate: '2019-04-29', byAuthor: 'Daniel Bugl' } }
  • { expandPosts: true, filter: { fromDate: '2019-04-30', byAuthor: 'Daniel Bugl' } }

现在,看一下如下的代码:

{ type: 'CHANGE_FILTER', all: true }

如前面的代码所示,如果我们调度另一个 action ,则状态将恢复为 all 字符串,就像它处于初始状态一样。

Reducer

现在,我们仍然需要定义处理这些状态更改的函数。这样的函数称为 reducer 函数。它将 stateaction 作为参数,并返回新状态。

如果您了解 Redux 库,那么您已经非常熟悉 state、action 和 reducer 的概念。

现在,我们将定义我们的 reducer 函数:

  1. 我们从 reducer 的函数定义开始:
function reducer(state, action){
  1. 然后,我们使用 switch 语句检查 action.type
switch (action.type) {
  1. 现在,我们将处理 TOGGLE_EXPAND action,我们只需切换当前的 expandPosts 状态:
case 'TOGGLE_EXPAND':
 return { ...state, expandPosts: !state.expandPosts }
  1. 接下来,我们将处理 CHANGE_FILTER action。在这里,我们首先需要检查 all 是否设置为 true,在这种情况下,只需将我们的过滤器设置为 'all' 字符串:
case 'CHANGE_FILTER':
  if (action.all) {
    return { ...state, filter: 'all' }
  }
  1. 现在,我们必须处理其他 filter 选项。首先,我们检查 filter 变量是否已经是一个 object。如果没有,我们会创建一个新的。否则,我们使用现有对象:
let filter = typeof state.filter === 'object' ? 
  state.filter : {}
  1. 然后,我们为各种过滤器定义处理程序,允许一次设置多个过滤器 ,而不是立即返回新 state
if (action.fromDate) {
  filter = { ...filter, fromDate: action.fromDate }
}

if (action.byAuthor) {
  filter = { …filter, byAuthor: action.byAuthor }
}
  1. 最后,我们返回新 state
return { ...state, filter }
  1. 对于 default 情况,我们抛出错误,因为这是一个未知的 action:
   default:
    throw new Error()
  }
}
在默认情况下抛出错误与 Redux reducer 的最佳实践不同,在默认情况下,我们只是返回 state 。因为 React Reducer Hook 不会将所有状态存储在一个对象中,所以我们只处理某些状态对象的某些 action,因此我们可以为未知 action 抛出错误。

现在,我们的已定义 reducer 函数,我们可以继续定义 Reducer Hook。

Reducer Hook

现在我们已经定义了 action 和 reducer 函数,我们可以从 reducer 创建一个 Reducer Hook。useReducer 的签名如下:

const [ state, dispatch ] = useReducer(reducer, initialstate)

我们唯一需要定义的是 initialstate;然后我们可以定义一个 Reducer Hook:

const initialstate = { all: true }

现在,我们可以通过使用从 Reducer Hook 返回的 state 对象来访问状态,并通过 dispatch 函数调度 action,如下所示:

dispatch({ type: 'TOGGLE_EXPAND' })

如果我们想给 action 添加其他选项,我们只需将它们添加到 action 对象:

dispatch({ type: 'CHANGE_FILTER', fromDate: '2019-04-30' })

正如我们所看到的,使用 action 和 reducer 处理状态变化比直接调整状态对象要容易得多。

实现 Reducer Hook

了解 action、reducer 和 Reducer Hook 之后,我们将在博客应用程序中实现它们。当状态对象或状态更改变得过于复杂时,任何现有的 State Hook 都可以转换为 Reducer Hook。

如果有多个 setState 函数总是同时调用,则最好将它们组合在一个 Reducer Hook 中。

全局状态通常是使用 Reducer Hook 而不是 State Hook,因为全局状态更改可能发生在应用程序中的任何位置。这样,处理 action 和更新状态逻辑就更容易了——只需要在一个地方修改。将所有状态更改逻辑放在一个地方可以更轻松地维护和修复错误,而不会因为忘记更新每处逻辑而引入新错误。

我们现在将把博客应用程序中的一些现有 State Hook 变成 Reducer Hook。

将 State Hook 变成 Reducer Hook

在我们的博客应用程序中,有两个全局 State Hook,我们将用 Reducer Hook 替换它们:

  • user 状态
  • posts 状态

我们首先替换 user State Hook。

替换用户 State Hook

我们将从 user State Hook 开始,因为它比文章 State Hook 更简单。稍后,user 状态将包含复杂的状态更改,因此在这里使用 Reducer Hook 是有意义的。

我们将先定义 action,然后再定义 reducer 函数。最后,我们将用 Reducer Hook 替换 State Hook。

定义 Action

我们从定义的 action 开始,因为它们在定义 reducer 函数时很重要。

现在让我们定义 action:

  1. 首先,我们需要一个操作来允许用户通过提供 usernamepassword 值进行登录:
{ type: 'LOGIN', username: 'Daniel Bugl', password: 'notsosecure' }
  1. 然后,我们还需要一个 REGIATER action,在这个例子中,它将类似于 LOGIN action,因为我们还没有实现任何注册逻辑:
{ 
  type: 'REGISTER'', 
  username: 'Daniel Bugl', 
  password: 'notsosecure', 
  passwordRepeat: 'notsosecure' 
}
  1. 最后,我们将需要一个 LOGOUT action,它只是用来注销当前登录的用户:
{ type: 'LOGOUT' }

现在,我们已经定义了所有必需的和用户相关 action,我们可以继续定义 reducer 函数。

定义 Reducer

接下来,我们为 user 状态定义一个 reducer 函数。现在,我们将把我们的 Reducer 放在 src/App.js 文件中。

稍后,创建一个单独的 src/reducers.js 文件,甚至创建一个单独的 src/reducers/ 目录,每个 reducer 函数都有单独的文件,这可能是有意义的。

让我们开始定义 userReducer 函数:

  1. src/App.js 文件中,在 App 函数定义之前,为 user 状态创建一个 userReducer 函数:
function userReducer (state, action) {
  1. 同样,我们对 action 类型使用 switch 语句:
switch (action.type) {
  1. 然后,我们处理 LOGIN 和 REGIATER action,我们将 user 状态设置为给定的 username 值。在我们的例子中,现在只是从 action 对象返回 username 值:
case 'LOGIN':
case 'REGIsTER':
  return action.username
  1. 接下来,我们处理 LOGOUT action,将状态设置为空字符串:
case 'LOGOUT': return ''
  1. 最后,当遇到未处理的操作时,我们会抛出一个错误:
   default:
    throw new Error()
  }
}

现在,定义了 userReducer 函数,我们可以继续定义 Reducer Hook。

定义 Reducer Hook

在定义了 action 和 reducer 函数之后,我们将定义 Reducer Hook,并将其状态和 dispatch 函数传递给需要它的组件。

让我们开始实现 Reducer Hook:

  1. 首先,我们必须通过调整 src/App.js 中以下语句来导入 useReducer Hook:
import React, { usestate, useReducer } from 'react'
  1. 编辑 src/App.js,并删除以下 State Hook:
const [ user, setUser ] = usestate('')

将前面的 State Hook 替换为 Reducer Hook - 初始状态是一个空字符串,就像之前一样:

const [ user, dispatchUser ] = useReducer(userReducer, '')
  1. 现在,将 user 状态和 dispatchUser 函数(作为 dispatch prop) 传递给 UserBar 组件:
<UserBar user={user} dispatch={dispatchUser} />
  1. 我们不需要修改 CreatePost 组件,因为我们只是将 user 状态传递给它,而该部分没有更改。
  2. 接下来,我们在 src/user/UserBar.js 中编辑 UserBar 组件,并将 setUser prop 替换为 dispatch 函数:
export default function UserBar({ user, dispatch }) {
  if (user) {
    return <Logout user={user} dispatch={dispatch} />
  } else {
    return (
      <React.Fragment>
        <Login dispatch={dispatch} />
        <Register dispatch={dispatch} />
      </React.Fragment>
    )
  }
}
  1. 现在,我们可以在 src/user/Login.js 中编辑 Login 组件,并将 setUser 函数替换为 dispatch 函数:
export default function Login ({ dispatch }) {
  1. 然后,我们将对 setUser 的调用替换为对 dispatch 函数的调用,调度一个 LOGIN action:
<form onSubmit={e => {
  e.preventDefault();
  dispatch({
    type:'LOGIN',
    username
  })
}}>
我们还可以创建返回 action 的函数,即所谓的 action creators。与其每次都手动创建 action 对象,我们可以简单地调用 loginAction('username'),它会返回相应的 LOGIN Action 对象。
  1. 我们对 src/user/Register.js 中的 Register 组件重复相同的过程:
export default function Register({ dispatch }) {
  // ...
  <form onSubmit={ e => {
    e.preventDefault();
    dispatch({
      type: 'REGISTER',
      username
    })
  }}>
  1. 最后,我们还对 src/user/Logout.js 中的 Logout 组件重复相同的过程:
export default function Logout({ user, dispatch }) {
  // ...
  <form onSubmit={e => {
    e.preventDefault();
    dispatch({ type: 'LOGOUT' })
  }}>

现在,我们的应用程序应该像以前一样工作,但它使用 Reducer Hook 而不是简单的 State Hook!

替换文章 State Hook

posts 状态使用 Reducer Hook 也很有意义,因为稍后我们将拥有可用于删除和编辑文章的功能,因此保留这些复杂的状态更改非常有意义。

现在让我们开始用 Reducer Hook 替换文章 State Hook。

定义 Action

同样,我们从定义 action 开始。目前,我们只考虑一个 CREATE_POST action:

{
  type: 'CREATE_POST',
  title: 'React Hooks',
  content: 'The greatest thing since sliced bread!',
  author: 'Daniel Bugl'
}

这是我们目前需要对文章使用的唯一 action。

定义 Reducer

接下来,我们将以和 user 状态类似的方式定义 reducer 函数:

  1. 首先我们编辑 src/App.js,并在那里定义 reducer 函数。以下代码定义了 postReducer 函数:
function postsReducer (state, action) {
  switch (action.type) {
  1. 在此函数中,我们将处理 CREATE_POST action。首先创建一个 newPost 对象,然后使用展开运算符语法将其插入当前 posts 状态的开头,方法与之前在 src/post/CreatePost.js 组件中所使用的类似:
case 'CREATE_POST':
  const newPost = {
    title: action.title,
    content: action.content,
    author: action.author
  }
  return [ newPost, ...state ]
  1. 这将是我们在此 Reducer 中处理的唯一 action,因此我们现在可以定义 default 语句:
   default:
   throw new Error()
 }
}

现在,定义了 posts reducer 函数,我们可以继续创建 Reducer Hook。

定义 Reducer Hook

最后,我们将定义并使用 Reducer Hook 作为 posts 状态:

  1. 我们首先在 src/App.js 中删除以下 State Hook:
const [ posts, setPosts ] = usestate(defaultPosts)

我们用以下 Reducer Hook 替换它:

const [ posts, dispatchPosts ] = useReducer(postsReducer, defaultPosts)
  1. 然后,我们将 dispatchPosts 函数作为 dispatch prop 传递给 CreatePost 组件:
{user && <CreatePost
  user={user}
  posts={posts}
  dispatch={dispatchPosts}
/>}
  1. 接下来,我们在 src/post/CreatePost.js 中编辑 CreatePost 组件,并将 setPosts 函数替换为 dispatch 函数:
export default function CreatePost ({ user, posts, dispatch }) {
  1. 最后,我们在 handleCreate 函数中使用 dispatch 函数:
function handleCreate () {
  dispatch({
    type: 'CREATE_POST',
    title, content,
    author: user
  })
}

现在,posts 状态也使用 Reducer Hook 而不是 State Hook,它的工作方式与以前相同!但是,如果我们想稍后添加更多用于管理文章的逻辑,例如搜索、过滤、删除和编辑,这样做会容易得多。

示例代码

在我们的博客应用程序中使用 Reducer Hook 的示例代码可以在 Chapter04/chapter4_1 文件夹中找到。

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

合并 Reducer Hook

目前,我们有两个不同的 dispatch 函数:一个用于 user 状态,一个用于 posts 状态。在我们的例子中,将两个 Reducer 合二为一,然后调用进一步的 Reducer,以处理子状态是有意义的。

这种模式类似于 Reducer 在 Redux 中的工作方式,其中我们只有一个对象包含应用程序的整个状态树,这在全局状态的情况下是有意义的。但是,对于复杂的本地状态更改,将 Reducer 分开可能更有意义。

让我们开始将所有 reducer 函数合并到一个 reducer 函数中。我们在这里将所有 Reducer 重构为 src/reducers.js 文件,以使 src/App.js 文件更具可读性:

  1. 新建 src/reducers.js 文件。
  2. src/App.js 文件中剪切以下代码,并将其粘贴到 src/reducers.js 文件中:
function userReducer(state, action) {
  switch (action.type) {
    case 'LOGIN':
    case 'REGISTER':
      return action.username
    case 'LOGOUT':
      return ''
    default:
      throw new Error()
  }
}

function postsReducer(state, action) {
  switch (action.type) {
    case 'CREATE_POST':
      const newPost = {
        title: action.title, content:
          action.content, author: action.author
      }
      return [newPost, ...state]
    default:
      throw new Error()
  }
}
  1. 编辑 src/reducers.js,并在现有 reducer 函数下面定义一个新的 reducer 函数,称为 appReducer
export default function appReducer (state, action) {
  1. appReducer 函数中,我们将调用其他两个 reducer 函数,并返回完整的状态树:
  return {
    user: userReducer(state.user, action),
    posts: postsReducer(state.posts, action)
  }
}
  1. 编辑 src/App.js,并在此处导入 appReducer
import appReducer from './reducers'
  1. 然后,我们删除以下两个 Reducer Hook 定义:
const [ user, dispatchUser ] = useReducer(userReducer, '')
const [ posts, dispatchPosts = useReducer(postsReducer, defaultPosts)

我们将前面的 Reducer Hook 定义替换为 appReducer 的单个 Reducer Hook 定义:

const [ state, dispatch ] = useReducer(appReducer, { user: '', posts: defaultPosts })
  1. 接下来,我们使用解构从 state 对象中提取 userposts 的值:
const { user, posts } = state
  1. 现在,我们仍然需要用 dispatch 函数替换我们传递给其他组件的 dispatchUserdispatchPosts 函数:
<UserBar user={user} dispatch={dispatch} />
{user && <CreatePost
 user={user}
 posts={posts}
 dispatch={dispatch}
/>}

如我们所见,现在只有一个 dispatch 函数和一个状态对象。

忽略未处理的 Action

但是,如果我们现在尝试登录,将看到来自 postsReducer 的错误。这是因为我们仍然在未处理的操作上抛出错误。为了避免这种情况,我们必须忽略未处理的操作,并简单地返回 state

src/reducers.js 中编辑 userReducerpostReducer 函数,并删除以下代码:

default:
 throw new Error()

将前面的代码替换为返回 statereturn 语句:

default:
 return state

正如我们所看到的,现在我们的应用程序仍然以与以前完全相同的方式工作,但我们在整个应用程序状态中使用了一个单一的 Reducer!

示例代码

在我们的博客应用程序中使用单个 Reducer Hook 的示例代码可以在 Chapter04/chapter4_2 文件夹中找到。

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

使用 Effect Hook

我们将经常使用的最后一个基本 Hook 是 Effect Hook。使用 Effect Hook,我们可以从组件中执行副作用,例如在组件挂载或更新时获取数据。

以我们的博客为例,我们将实现一项功能,该功能可在登录时更新网页标题,使其包含当前登录用户的用户名。

还记得 componentDidMount 和 componentDidUpdate 吗?

如果你以前使用过 React,你可能已经使用过 componentDidMountcomponentDidUpdate 生命周期方法。例如,我们可以使用 React 类组件将文档标题设置为给定的 prop,如下所示。在下面的代码片段中,第 4 ~ 7 行显示为生命周期方法:

import React from 'react'

class App extends React.Component {
  componentDidMount() {
    const { title } = this.props
    document.title = title
  }
  render() {
    return (
      <div>Test App</div>
    )
  }
}

这很好。但是,当 title prop 更新时,更改不会反映在我们网页的标题中。为了解决这个问题,我们需要定义 componentDidUpdate 生命周期方法(第 9 ~ 14 行),如下所示:

import React from 'react'

class App extends React.Component {
  componentDidMount() {
    const { title } = this.props
    document.title = title
  }

  componentDidUpdate(prevProps) {
    const { title } = this.props
    if (title !== prevProps.title) {
      document.title = title
    }
  }

  render() {
    return (
      <div>Test App</div>
    )
  }
}

您可能已经注意到,我们编写了两次几乎相同的代码;因此,我们可以创建一个新方法来处理 title 的更新,然后在两个生命周期方法中调用它。在下面的代码片段中,第 4 ~ 7行、第 9 ~ 11行、第 13 ~ 17 为更新代码:

import React from 'react'

class App extends React.Component {
  updateTitle() {
    const { title } = this.props
    document.title = title
  }

  componentDidMount() {
    this.updateTitle()
  }

  componentDidUpdate(prevProps) {
    if (this.props.title !== prevProps.title) {
      this.updateTitle()
    }
  }

  render() {
    return (
      <div>Test App</div>
    )
  }
}

但是,我们仍然需要调用 this.updateTitle() 两次。当我们稍后更新代码时,例如,将参数传递给 this.updateTitle(),我们始终需要记住在对该方法的两次调用中都将其传递给它。如果我们忘记更新其中一个生命周期方法,都可能会引入错误。此外,我们需要在 componentDidUpdate 中添加一个 if 条件,以避免在 title prop 没有更改时调用 this.updateTitle()

使用 Effect Hook

在 Hook 的世界里,componentDidMountcomponentDidUpdate 生命周期方法组合在 useEffect Hook 中,当不指定依赖数组时,只要组件中的任何 props 发生变化,就会触发。

因此,我们现在可以使用 Effect Hook 定义一个函数组件,而不是使用类组件,它执行与以前相同的操作。传递给 Effect Hook 的函数称为“副作用函数”:

import React, { useEffect } from 'react'

function App({ title }) {
  useEffect(() => {
    document.title = title
  })
  return (
    <div>Test App</div>
  )
}

这就是我们需要做的!我们定义的 Hook 将在每次任何 props 更改时调用我们的 Effect 函数。

仅在某些 Props 发生变化时触发 Effect

如果我们想确保我们的 effect 函数仅在标题 props 更改时才被调用,例如,出于性能原因,我们可以指定哪些值应该触发更改,作为 useEffect Hook 的第二个参数:

useEffect(() => {
  document.title = title
}, [title])

这不仅限于 props,我们在这里可以使用任何值,甚至是来自其他 Hook 的值,例如 State Hook 或 Reducer Hook:

const [ title, setTitle ] = usestate('')
useEffect(() => {
  document.title =  title
}, [title])

如我们所见,在处理不断变化的值时,使用 Effect Hook 比使用生命周期方法简单得多。

仅在挂载时触发 Effect

如果我们想复制只添加一个 componentDidMount 生命周期方法的行为,而不在 props 更改时触发,我们可以通过向 useEffect Hook传递一个空数组作为第二个参数来实现:

useEffect(() => {
  document.title = title
}, [])

传递一个空数组意味着我们的 effect 函数只会在组件挂载时触发一次,并且在 props 更改时不会触发。但是,与其考虑组件的挂载,使用 Hook,我们应该考虑 effect 的依赖关系。在这种情况下,effect 没有任何依赖项,这意味着它只会触发一次。如果 effect 指定了依赖项,则当任何依赖项更改时,它将再次触发。

清理 Effect

有时,在卸载组件时需要清理 effect。为此,我们可以从 Effect Hook 的 effect 函数返回一个函数。此返回函数的工作方式类似于 componentWillUnmount 生命周期方法:

useEffect(() => {
  const updateInterval = setInterval(() => console.log('fetching update'), updateTime)
  return () => clearInterval(updateInterval)
}, [updateTime])

上面代码第 3 行称为清理函数。清理函数将在组件卸载之后再次运行 effect 之前调用。例如,这避免了 updateTime props 更改时的错误。在这种情况下,将清除以前的 effect,并使用更新后的 updateTime 值定义一个新的间隔。

在我们的博客应用中实现 Effect Hook

现在我们已经了解了 Effect Hook 的工作原理,我们将在我们的博客应用程序中使用它,以便在我们登录/注销时(当 user 状态更改时)实现标题更改。

让我们开始在博客应用程序中实现 Effect Hook:

  1. 编辑 src/App.js,并导入 useEffect Hook:
import React, { useReducer, useEffect } from 'react'
  1. 在定义了 useReducer Hook 和状态解构后,定义一个 useEffect Hook,它根据 username 值调整 document.title 变量:
useEffect(() -> {
  1. 如果用户已登录,我们将 document.title 设置为 <username> - React Hooks Blog。为此,我们使用模板字符串,这允许我们通过 ${} 语法在字符串中包含变量或 JavaScript 表达式。模板字符串使用 \` 定义:
if (user) {
  document.title =  `${user} - React Hooks Blog`
  1. 否则,如果用户没有登录,我们只需将 document.title 设置为 React Hooks Blog
} else {
  document.title  = 'React Hooks Blog'
}
  1. 最后,我们将 user 值作为第二个参数传递给 Effect Hook,以确保每当 user 值更新时,我们的 Effect 函数都会再次触发:
}, [user])

如果我们现在启动应用程序,我们可以看到文档 document.title 被设置为 React Hooks Blog ,因为 Effect Hook 在应用程序组件挂载时触发,而 user 值还没有定义 :

title-default.png
Effect Hook 的 effect:网页标题更改中

使用 Test User 登录后,我们可以看到 document.title 更改为 Test User - React Hooks Blog

title-test-user.jpg
Effect Hook 会在 user 值改变后重新触发 effect

如我们所见,我们的 Effect Hook user 值更改后成功重新触发!

示例代码

在我们的博客应用程序中实现 Effect Hook 的示例代码可以在 Chapter04/chapter4_3 文件夹中找到。

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

总结

在本章中,我们首先了解了 action、reducer 和 Reducer Hook。我们还学习了何时应该使用 Reducer Hook 而不是 State Hook。然后,我们将 userposts 状态的现有全局 State Hook 替换为两个 Reducer Hook。接下来,我们将两个 Reducer Hook 合并为一个 app Reducer Hook。最后,我们了解了 Effect Hook,以及如何使用它们来代替 componentDidMountcomponentDidUpdate

在下一章中,我们将学习 React context,以及如何将其与 Hook 一起使用。然后,我们将向应用程序添加 Context Hook,以避免在多层组件上传递 props。

问题

为了回顾我们在本章中学到的内容,请尝试回答以下问题:

  1. State Hook 的常见问题是什么?
  2. 什么是 action?
  3. 什么是 reducer?
  4. 什么时候我们应该使用 Reducer Hook 而不是 State Hook?
  5. 将 State Hook 转换为 Reducer Hook 需要哪些步骤?
  6. 我们如何才能更轻松地创建 action?
  7. 我们什么时候应该合并 Reducer Hook?
  8. 合并 Reducer Hook 时需要注意什么?
  9. 类组件中的什么等效于 Effect Hook?
  10. 与类组件相比,使用 Effect Hook 有什么优势?

延伸阅读

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

1

评论 (0)

取消