第 11 章 从 React 类组件迁移

第 11 章 从 React 类组件迁移

Flying
2021-05-06 / 0 评论 / 125 阅读 / 正在检测是否收录...

在上一章中,我们学习了如何通过从现有代码中提取自定义 Hooks 来构建我们自己的 Hooks。然后,我们在博客应用程序中使用了我们自己的 Hooks,并了解了本地 Hooks 和 Hooks 之间的交互。最后,我们学习了如何使用 React 的 Hooks 测试库为 Hooks 编写测试,并为我们的自定义 Hooks 实现了测试。

在本章中,我们将首先使用 React 类组件实现一个 ToDo 应用程序。在下一步中,我们将学习如何将现有的 React 类组件应用程序迁移到 Hooks。在实践中看到使用 Hooks 的函数组件和类组件之间的差异将加深我们对使用任一解决方案的权衡的理解。此外,在本章结束时,我们将能够将现有的 React 应用程序迁移到 Hooks。

本章将介绍以下主题:

  • 使用类组件处理状态
  • 将应用程序从类组件迁移到 Hooks
  • 了解类组件与 Hooks 的权衡

技术要求

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

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

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

http://bit.ly/2Mm9yoC

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

使用类组件处理状态

在我们开始从类组件迁移到 Hooks 之前,我们将使用 React 类组件创建一个小型的 ToDo 列表应用程序。在下一节中,我们将使用 Hooks 将这些类组件转换为函数组件。最后,我们将比较这两种解决方案。

设计应用程序结构

正如我们之前对博客应用程序所做的那样,我们将从考虑应用程序的基本结构开始。对于此应用程序,我们将需要以下功能:

  • 页眉
  • 添加新待办事项的方法
  • 一种在列表中显示所有待办事项的方法
  • 待办事项的筛选器

从模型开始总是一个好主意。那么,让我们开始吧:

  1. 我们首先为我们的 ToDo 应用程序绘制一个界面模型:

ch11-todo.png
我们的待办事项应用程序的模型

  1. 接下来,我们定义基本组件,其方式类似于我们使用博客应用程序的方式:

![ch11-fundamental-comp.png](/uploads/2105/ch11-fundamental-comp.png
在我们的应用程序模型中定义基本组件

  1. 现在我们可以定义容器组件:

ch11-container-comp.png
在我们的应用程序模型中定义容器组件

如我们所见,我们将需要以下组件:

  • App
  • Header
  • AddTodo
  • TodoList
  • TodoItem
  • TodoFilter (+ TodoFilterItem)

TodoList 组件使用用于显示项目的 TodoItem 组件,其中包含要完成的复选框和用于删除它的按钮。TodoFilter 组件在内部使用 TodoFilterItem 组件来显示各种过滤器。

初始化项目

我们将使用 create-react-app 来创建一个新项目。现在让我们初始化项目:

  1. 运行以下命令:
npx create-react-app chapter11_1
  1. 然后,删除 src/App.css,因为我们不需要它。
  2. 接下来,编辑 src/index.css,并按如下方式调整边距:
margin: 20px;
  1. 最后,删除当前的 src/App.js 文件,因为我们将在下一步中创建一个新文件。

现在,我们的项目已经初始化,我们可以开始定义应用程序结构了。

定义应用程序结构

我们已经从模型中知道应用程序的基本结构会是什么样子,所以让我们从定义 App 组件开始:

  1. 创建一个新的 src/App.js 文件。
  2. 导入 ReactHeaderAddTodoTodoListTodoFilter 组件:
import React from 'react'
import Header from './Header'
import AddTodo from './AddTodo'
import TodoList from './TodoList'
import TodoFilter from './TodoFilter'
  1. 现在将 App 组件定义为类组件。现在,我们只定义 render 方法:
export default class App extends React.Component {
  render() {
    return (
      <div style={{ width: 400 }}>
        <Header />
        <AddTodo />
        <hr />
        <TodoList />
        <hr />
        <TodoFilter />
      </div>
    )
  }
}

App 组件定义了我们应用程序的基本结构。它将包含一个标题、一种添加新待办事项的方法、一个待办事项列表和一个过滤器。

定义组件

现在,我们将组件定义为静态组件。在本章的后面,我们将为它们实现动态功能。现在,我们将实现以下静态组件:

  • Header
  • AddTodo
  • TodoList
  • TodoItem
  • TodoFilter

现在让我们开始实现组件。

定义 Header 组件

我们将从 Header 组件开始,因为它是所有组件中最简单的:

  1. 创建一个新的 src/Header.js 文件。
  2. 导入 React 并使用 render 方法定义类组件:
import React from 'react'
export default class Header extends React.Component {
  render() {
    return <h1>ToDo</h1>
  }
}

现在,我们应用程序的 Header 组件已定义。

定义 AddTodo 组件

接下来,我们将定义 AddTodo 组件,它渲染一个 input 字段和一个按钮。

现在让我们实现 AddTodo 组件:

  1. 创建一个新的 src/AddTodo.js 文件。
  2. 导入 React 并定义类组件和 render 方法:
import React from 'react'

export default class AddTodo extends React.Component {
    render () {
        return (
  1. render 方法中,我们返回一个包含 input 字段和一个添加按钮的 form
      <form>
        <input type="text" placeholder="enter new task..." style={{ width: 350, height: 15 }} />
        <input type="submit" style={{ float: 'right', marginTop: 2 }} value="add" />
      </form>
    )
  }
}

如我们所见,AddTodo 组件由一个 input 字段和一个按钮组成。

定义 TodoList 组件

现在,我们定义 TodoList 组件,它将使用 TodoItem 组件。现在,我们将在此组件中静态定义两个待办事项。

让我们开始定义 TodoList 组件:

  1. 创建一个新的 src/TodoList.js 文件。
  2. 导入 ReactTodoItem 组件:
import React from 'react'
import TodoItem from './TodoItem'
  1. 然后,定义类组件和 render 方法:
export default class TodoList extends React.Component {
  render () {
  1. 在这个 render 方法中,我们静态地定义两个待办事项:
const items = [
  { id: 1, title: 'Write React Hooks book', completed: true },
  { id: 2, title: 'Promote book', completed: false },
]
  1. 最后,我们将使用 map 函数渲染事项列表:
    return items.map(item =>
      <TodoItem {...item} key={item.id} />
    )
  }
}

如我们所见,TodoList 组件渲染了 TodoItem 组件的列表。

定义 TodoItem 组件

定义 TodoList 组件后,现在我们将定义TodoItem 组件,以便渲染单个事项。让我们开始定义 TodoItem 组件:

  1. 创建一个新的 src/TodoItem.js 组件。
  2. 导入 React,并定义组件,以及 render 方法:
import React from 'react'

export default class TodoItem extends React.Component {
  render () {
  1. 现在,我们将使用解构来获得 titlecompleted props:
const { title, completed } = this.props
  1. 最后,我们将渲染一个包含 checkboxtitlebutton(用来删除该事项) 的 div 元素:
    return (
      <div style={{ width: 400, height: 25 }}>
        <input type="checkbox" checked={completed} />
        {title}
        <button style={{ float: 'right' }}>x</button>
      </div>
    )
  }
}

TodoItem 组件由一个复选框、一个 title 和一个用于删除项目的 button 组成。

定义 TodoFilter 组件

最后,我们将定义 TodoFilter 组件。在同一文件中,我们将为 TodoFilterItem 定义另一个组件。

让我们开始定义 TodoFilterItemTodoFilter 组件:

  1. 创建一个新的 src/TodoFilter.js 文件。
  2. TodoFilterItem 定义一个类组件:
class TodoFilterItem extends React.Component {
  render () {
  1. 在这个渲染方法中,我们使用解构来获取 name prop:
const { name } = this.props
  1. 接下来,我们将为 style 定义一个对象:
const style = {
  color: 'blue',
  cursor: 'pointer',
}
  1. 然后,我们返回一个带有 name 过滤器值的 span 元素,并使用定义的 style 对象:
    return <span style={style}>{name}</span>
  }
}
  1. 最后,我们可以定义实际的 TodoFilter 组件,它将渲染三个 TodoFilterItem 组件,如下所示:
export default class TodoFilter extends React.Component {
  render() {
    return (
      <div>
        <TodoFilterItem name="all" />
        {' / '}
        <TodoFilterItem name="active" />
        {' / '}
        <TodoFilterItem name="completed" />
      </div>
    )
  }
}

现在,我们有一个组件列出了三种不同的过滤器可能性:allactivecompleted

实现动态代码

现在我们已经定义了所有静态组件,我们的应用程序应该看起来就像模型一样。下一步是使用 React 状态、生命周期和处理方法实现动态代码。

在本节中,我们将执行以下操作:

  • 定义模拟 API
  • 定义 StateContext
  • 使 App 组件动态化
  • 使 AddTodo 组件动态化
  • 使 TodoList 组件动态化
  • 使 TodoItem 组件动态化
  • 使 TodoFilter 组件动态化

让我们开始吧。

定义 API 代码

首先,我们将定义一个获取待办事项的 API。在我们的例子中,我们只是在短暂的延迟后返回一系列待办事项。

让我们开始实现模拟 API:

  1. 创建一个新的 src/api.js 文件。
  2. 我们将定义一个函数,该函数将根据通用唯一标识符(UUID)函数为我们的待办事项生成随机 ID:
export const generateID = () => {
  const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
  return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4()
}
  1. 然后,我们定义 fetchAPITodos 函数,它返回一个 Promise,该函数在短暂延迟后解析:
export const fetchAPITodos = () =>
  new Promise((resolve) =>
    setTimeout(() =>
      resolve([
        { id: generateID(), title: 'Write React Hooks book', completed: true },
        { id: generateID(), title: 'Promote book', completed: false },
      ]), 100)
  )

现在,我们有一个函数,通过在 100 毫秒的延迟后返回一个数组来模拟从 API 获取待办事项。

定义 StateContext

接下来,我们将定义一个保留当前的待办事项列表的 context。我们将该 context 称为 StateContext

现在让我们开始实现 StateContext

  1. 创建一个新的 src/StateContext.js 文件。
  2. 导入 React,如下所示:
import React from 'react'
  1. 现在,定义 StateContext 并设置一个空数组作为备用值:
const StateContext = React.createContext([])
  1. 最后,导出 StateContext
export default StateContext

现在,我们有一个可以存储待办事项数组的 context。

使 App 组件动态化

我们现在将通过添加获取、添加、切换、过滤和删除待办事项的功能,使 App 组件动态化。此外,我们将定义一个 StateContext 提供程序。

让我们开始使 App 组件动态化:

  1. src/App.js 中,在其他导入语句之后导入 StateContext
import StateContext from './StateContext'
  1. 然后,在 src/api.js 文件中导入 fetchAPITodosgenerateID 函数:
import { fetchAPITodos, generateID } from './api'
  1. 接下来,我们将修改 App 类代码,实现一个 constructor,它将设置初始状态:
export default class App extends React.Component {
  constructor (props) {
  1. 在这个 constructor 中,我们需要首先调用 super,以确保父类(React.Component)构造函数被调用,并且组件被正确初始化:
super(props)
  1. 现在,我们可以通过设置 this.state 来设置初始状态。最初,将没有待办事项,filter 的值将设置为 all
  this.state = { todos: [], filteredTodos: [], filter: 'all' }
}
  1. 然后,我们定义 componentDidMount 生命周期方法,该方法将在组件首次渲染时获取待办事项:
componentDidMount () {
  this.fetchTodos()
}
  1. 现在,我们将定义实际的 fetchTodos 方法,在我们的例子中,它只是设置状态,因为我们不打算将这个简单的应用程序连接到后端。我们还将调用 this.filterTodos(),以便在获取 todos 后更新 filteredTodos 数组:
fetchTodos () {
  fetchAPITodos().then((todos) => {
    this.setState({ todos })
    this.filterTodos()
  })
}
  1. 接下来,我们定义 addTodo 方法,该方法创建一个新项,并将其添加到状态数组中,类似于我们在博客应用中使用 Hooks 所做的:
addTodo (title) {
    const { todos } = this.state
    const newTodo = { id: generateID(), title, completed: false }

    this.setState({ todos: [ newTodo, ...todos ] })
    this.filterTodos()
  }
  1. 然后,我们定义 toggleTodo 方法,该方法使用 map 函数来查找和修改某个待办事项:
toggleTodo (id) {
  const { todos } = this.state
  const newTodos = todos.map(t => {
    if (t.id === id) {
      return { ...t, completed: !t.completed }
    }
    return t
  }, [])
  this.setState({ todos: newTodos }) this.filterTodos()
}
  1. 现在,我们定义 removeTodo 方法,它使用 filter 方法来查找和删除某个待办事项:
removeTodo (id) {
  const { todos } = this.state
  const newTodos = todos.filter(t => {
    if (t.id === id) {
      return false
    }
    return true
  })
  this.setState({ todos: newTodos })
  this.filterTodos()
}
  1. 然后,我们定义一个方法来对待办事项应用特定的 filter
applyFilter(todos, filter) {
  switch (filter) {
    case 'active':
      return todos.filter((t) => t.completed === false)
    case 'completed':
      return todos.filter((t) => t.completed === true)
    default:
    case 'all':
      return todos
  }
}
  1. 现在,我们可以定义 filterTodos 方法,它将调用 applyFilter 方法,并更新 filteredTodos 数组和 filter 值:
filterTodos(filterArg) {
  this.setState(({ todos, filter }) => ({
    filter: filterArg || filter,
    filteredTodos: this.applyFilter(todos, filterArg || filter)
  }))
}
我们正在使用 filterTodos,以便在添加/删除项目以及更改过滤器后重新过滤待办事项。为了使这两个功能正常工作,我们需要检查 filter 参数 filterArg 是否已传递。如果没有,我们从 state 回退到当前 filter 参数。
  1. 然后,我们调整 render 方法,以便使用 state 为 StateContext 提供值,并将某些方法传递给组件:
render () {
  const { filter, filteredTodos } = this.state
  return (
    <Statecontext.Provider value={filteredTodos}>
  1. 最后,我们需要将 this 重新绑定到类,以便我们可以将方法传递给我们的组件,而无需更改 this context。按如下方式调整constructor
constructor() {
  super(props)
  this.state = {
    todos: [], filteredTodos: [], filter: 'all'
  }
  this.fetchTodos = this.fetchTodos.bind(this)
  this.addTodo = this.addTodo.bind(this)
  this.toggleTodo = this.toggleTodo.bind(this)
  this.removeTodo = this.removeTodo.bind(this)
  this.filterTodos = this.filterTodos.bind(this)
}

现在,我们的 App 组件可以动态获取、添加、切换、删除和过滤待办事项。如我们所见,当我们使用类组件时,我们需要重新绑定类处理函数的 this contex。

使 AddTodo 组件动态化

在使我们的 App 组件动态化之后,是时候将所有其他组件动态化为 wel1 了。我们将从顶部开始,使用 AddTodo 组件。

现在让我们使 AddTodo 组件动态化:

  1. src/AddTodo.js 中,我们首先定义了一个 constructor,它为 input 字段设置初始 state
export default class AddTodo extends React.Component {
  constructor (props) {
  super(props)
  this.state = { input: ''}
}
  1. 然后,我们定义一个处理 input 字段中更改的方法:
handleInput (e) {
  this.setState({ input: e.target.value })
}
  1. 现在,我们将定义一个可以处理正在添加的新待办事项的方法:
handleAdd () {
  const { input } = this.state const { addTodo } = this.props
  if (input) {
    addTodo(input) this.setState({ input: '' })
  }
}
  1. 接下来,我们可以将状态值和处理方法分配给 input 字段和按钮:
render() {
  const { input } = this.state
  return (
    <form onSubmit={e => {
      e.preventDefault();
      this.handleAdd( )
    }}>
      <input
        type="text"
        placeholder="enter new task..."
        style={{ width: 350, height: 15 }}
        value={input}
        onChange={this.handleInput}
      />
      <input
        type="submit"
        style={{ float: 'right', marginTop: 2 }}
        disabled={!input}
        value="add"
      />
    </form>
  )
}
  1. 最后,我们需要调整 constructor 以重新绑定 this
constructor() {
  super(props)
  this.state = {
    input: ''
  }
  this.handleInput = this.handleInput.bind(this)
  this.handleAdd = this.handleAdd.bind(this)
}

现在,只要不输入文本,我们的 AddTodo 组件就会显示一个禁用的按钮。当激活时,单击按钮将触发从 App 组件传递过来的 handleAdd 函数。

使 TodoList 组件动态化

我们的 ToDo 应用程序中的下一个组件是 TodoList 组件。在这里,我们只需要从 StateContext 中获取待办事项。

现在让我们使 TodoList 组件动态化:

  1. src/TodoList.js 中,我们首先导入 StateContext,位于 TodoItem 导入语句下方:
import StateContext from './StateContext'
  1. 然后,我们将 contextType 设置为 StateContext,这将允许我们通过 this.context 访问 context:
export default class TodoList extends React.Component {
  static contextType = Statecontext
对于类组件,如果我们想使用多个 contexts,我们必须使用 StateContext.Consumer 组件,如下所示:
<StateContext.Consumer>{value => <div>State is:{value}</div>}</StateContext.Consumer>

可以想象,使用这样的多个 contexts 将导致一个非常深的组件树(包装器地狱),并且我们的代码将难以阅读和重构。

  1. 现在,我们可以从 this.context 获取项目,而不是静态定义它们:
render () {
  const items = this.context
  1. 最后,我们将所有 props 传递给 TodoItem 组件,以便我们可以在那里使用 removeTodotoggleTodo 方法:
return items.map(item =>
    <TodoItem {...item} {...this.props} key={item.id} />
  )
}

现在,我们的 TodoList 组件从 StateContext 中获取项目,而不是静态定义它们。

使 TodoItem 组件动态化

现在我们已经将 removeTodotoggleTodo 方法作为 props 传递给了 TodoItem 组件,我们可以在那里实现这些功能。

现在让我们使 TodoItem 组件动态化:

  1. src/TodoItem.js 中,我们首先定义 toggleTodoremoveTodo 函数的处理方法:
handleToggle () {
  const { toggleTodo, id } = this.props toggleTodo(id)
}

handleRemove () {
  const { removeTodo, id } = this.props removeTodo(id)
}
  1. 然后,我们将处理方法分别分配给 checkboxbutton
render() {
  const { title, completed } = this.props
  return (
    <div style={{ width: 400, height: 25 }}>
      <input type="checkbox" checked={completed} onChange=
        {this.handleToggle} />
      {title}
      <button style={{ float: 'right' }} onClick=
        {this.handleRemove}>x</button>
    </div>
  )
}
  1. 最后,我们需要重新为处理方法的绑定 this context。创建一个新的 constructor,如下所示:
export default class TodoItem extends React.Component {
  constructor (props) { super(props)
  this.handleToggle = this.handleToggle.bind(this)
  this.handleRemove = this.handleRemove.bind(this)
}

现在,TodoItem 组件触发切换和删除处理函数。

使 TodoFilter 组件动态化

最后,我们将使用 filterTodos 方法来动态过滤我们的待办事项列表。

让我们开始使 TodoFilter 组件动态化:

  1. src/TodoFilter.jsTodoFilter 类中,我们将所有 props 传递给 TodoFilterItem 组件:
export default class TodoFilter extends React.Component {
  render() {
    return (
      <div>
        <TodoFilterItem {...this.props} name="all" />
        {' / '}
        <TodoFilterItem {...this.props} name="active" />
        {' / '}
        <TodoFilterItem {...this.props} name="completed" />
      </div>
    )
  }
}
  1. src/TodoFilter.jsTodoFilterItem 类中,我们首先定义一个用于设置过滤器的处理方法:
handleFilter () {
  const { name, filterTodos } = this.props filterTodos(name)
}
  1. 然后我们从 TodoFilter 中获取 filter prop:
render () {
  const { name, filter = 'all' } = this.props
  1. 接下来,在以下代码块第 4 行中,我们使用 filter prop 以 bold(加粗)显示当前选择的过滤器:
const style = {
  color: 'blue',
  cursor: 'pointer',
  fontWeight: filter === name ? 'bold' : 'normal',
}
  1. 然后,我们通过 onClick 将处理方法绑定到筛选项:
return <span style={style} onClick={this.handleFilter}>{name}
  </span>
}
  1. 最后,我们为 TodoFilterItem 类创建一个新的 constructor,并重新绑定 this 上下文
class TodoFilterItem extends React.Component {
  constructor(props) {
    super(props)
    this.handleFilter = this.handleFilter.bind(this)
  }
}

现在,我们的 TodoFilter 组件触发了 handleFilter 方法以更改过滤器。我们的整个应用程序现在是动态的,我们可以使用它的所有功能。

示例代码

示例代码可以在 Chapter11/chapter11_1 文件夹中找到。

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


(节选)

0

评论 (0)

取消