React 挂钩学习手册(五)
原文:
zh.annas-archive.org/md5/0d61b163bb6c28fa00edc962fdaa2667译者:飞龙
第十三章:MobX 和 Hooks
在上一章中,我们学习了 Redux 以及如何将 Redux 与 Hooks 结合使用。我们还学习了如何将现有的 Redux 应用迁移到基于 Hook 的解决方案。此外,我们还了解了使用 Reducer Hooks 与 Redux 的权衡,以及何时使用其中之一。
在本章中,我们将学习如何将 MobX 与 Hooks 结合使用。我们将首先学习如何使用 MobX 处理状态,然后转而使用 Hooks 与 MobX。此外,我们还将学习如何将现有的 MobX 应用迁移到 Hooks。最后,我们将讨论使用 MobX 的利弊。通过本章的学习,您将完全了解如何使用 Hooks 编写 MobX 应用程序。
本章将涵盖以下主题:
-
了解 MobX 是什么以及它是如何工作的
-
使用 MobX 处理状态
-
使用 Hooks 与 MobX
-
迁移 MobX 应用
-
了解 MobX 的权衡
技术要求
应该已经安装了相当新的 Node.js 版本(v11.12.0 或更高)。还需要安装 Node.js 的npm包管理器。
本章的代码可以在 GitHub 存储库中找到:github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter13。
查看以下视频以查看代码的实际操作:
请注意,强烈建议您自己编写代码。不要简单地运行提供的代码示例。重要的是您自己编写代码,以便能够正确学习和理解它。但是,如果遇到任何问题,您可以随时参考代码示例。
现在,让我们开始本章。
什么是 MobX?
MobX 采用了与 Redux 不同的方法。它不是施加限制以使状态变化可预测,而是旨在自动更新从应用程序状态派生的任何内容。与分派动作不同,在 MobX 中,我们可以直接修改状态对象,MobX 将负责更新使用状态的任何内容。
MobX 的生命周期如下:
- 事件(如
onClick)调用动作,这是唯一可以修改状态的东西:
@action onClick = () => {
this.props.todo.completed = true
}
- 状态是可观察的,不应包含冗余或可推导的数据。状态非常灵活 - 它可以包含类、数组、引用,甚至可以是图:
@observable todos = [
{ title: 'Learn MobX', completed: false }
]
- 计算值是通过纯函数从状态派生出来的。这些将被 MobX 自动更新:
@computed get activeTodos () {
return this.todos.filter(todo => !todo.completed)
}
- 反应就像计算值,但它们也可以产生副作用,而不是一个值,比如在 React 中更新用户界面:
const TodoList = observer(({ todos }) => (
<div>
{todos.map(todo => <TodoItem {...todo} />)}
</div>
)
我们可以在以下图表中看到 MobX 的生命周期可视化:
MobX 生命周期的可视化
MobX 和 React 非常搭配。每当 MobX 检测到状态已经改变,它将导致适当的组件重新渲染。
与 Redux 不同,使用 MobX 并不需要学习很多限制。我们只需要了解一些核心概念,比如可观察值、计算值和反应。
现在我们知道了 MobX 的生命周期,让我们继续实践中使用 MobX 处理状态。
使用 MobX 处理状态
了解 MobX 最好的方法是在实践中使用它并看看它是如何工作的。所以,让我们从第十一章的 ToDo 应用程序开始,从 React 类组件迁移到 MobX。我们首先要做的是从Chapter11/chapter11_2/复制代码示例。
安装 MobX
第一步是通过npm安装 MobX 和 MobX React。执行以下命令:
> npm install --save mobx mobx-react
现在 MobX 和 MobX React 都安装好了,我们可以开始设置存储了。
设置 MobX 存储
安装完 MobX 后,现在是时候设置我们的 MobX 存储了。存储将存储所有状态,以及相关的计算值和操作。通常使用类来定义。
现在让我们定义 MobX 存储:
-
创建一个新的
src/store.js文件。 -
从 MobX 导入
observable、action和computed装饰器,以及decorate函数。这些将用于标记存储中的各种函数和值:
import { observable, action, computed, decorate } from 'mobx'
- 还要从我们的 API 代码中导入
fetchAPITodos和generateID函数:
import { fetchAPITodos, generateID } from './api'
- 现在,我们通过使用一个类来定义存储:
export default class TodoStore {
- 在这个存储中,我们存储了一个
todos数组和filter字符串值。这两个值都是可观察的。我们将在稍后将它们标记为这样:
todos = []
filter = 'all'
通过特殊的项目设置,我们可以使用一个实验性的 JavaScript 特性,称为装饰器,通过编写@observable todos = []来将我们的值标记为可观察的。然而,这种语法不受create-react-app支持,因为它还不是 JavaScript 标准的一部分。
- 接下来,我们定义一个计算值,以便从我们的 store 中获取所有经过过滤的
todos。该函数将类似于我们在src/App.js中的函数,但现在我们将使用this.filter和this.todos。同样,我们必须稍后将该函数标记为computed。MobX 将在需要时自动触发此函数,并存储结果,直到它所依赖的状态发生变化。
get filteredTodos () {
switch (this.filter) {
case 'active':
return this.todos.filter(t => t.completed === false)
case 'completed':
return this.todos.filter(t => t.completed === true)
default:
case 'all':
return this.todos
}
}
- 现在,我们定义我们的动作。我们从
fetch动作开始。与以前一样,我们必须稍后使用action装饰器标记我们的动作函数。在 MobX 中,我们可以通过设置this.todos直接修改我们的状态。因为todos值是可观察的,对它的任何更改都将被 MobX 自动跟踪:
fetch () {
fetchAPITodos().then((fetchedTodos) => {
this.todos = fetchedTodos
})
}
- 然后,我们定义了
addTodo动作。在 MobX 中,我们不使用不可变的值,因此不应创建新数组。相反,我们总是修改现有的this.todos值:
addTodo (title) {
this.todos.push({ id: generateID(), title, completed: false })
}
正如您所看到的,MobX 采用更加命令式的方法,直接修改值,MobX 会自动跟踪更改。我们不需要使用 rest/spread 语法来创建新数组;相反,我们直接修改现有状态数组。
- 接下来是
toggleTodo动作。在这里,我们循环遍历所有的todos并修改具有匹配id的项目。请注意,我们可以修改数组中的项目,并且更改仍将被 MobX 跟踪。事实上,MobX 甚至会注意到数组中只有一个值已经改变。结合 React,这意味着列表组件不会重新渲染;只有更改的项目组件将重新渲染。请注意,为了实现这一点,我们必须适当地拆分组件,例如制作单独的列表和项目组件:
toggleTodo (id) {
for (let todo of this.todos) {
if (todo.id === id) {
todo.completed = !todo.completed
break
}
}
}
for (let .. of ..) {结构将循环遍历数组的所有项目,或任何其他可迭代的值。
- 现在,我们定义了
removeTodo动作。首先,我们找到要删除的todo项目的index:
removeTodo (id) {
let index = 0
for (let todo of this.todos) {
if (todo.id === id) {
break
} else {
index++
}
}
- 然后,我们使用
splice来删除一个元素——从找到的元素的index开始。这意味着我们从数组中剪切具有给定id的项目:
this.todos.splice(index, 1)
}
- 我们定义的最后一个动作是
filterTodos动作。在这里,我们只需将this.filter值设置为新的过滤器:
filterTodos (filterName) {
this.filter = filterName
}
}
- 最后,我们必须使用我们之前提到的各种装饰器来装饰我们的 store。我们通过在我们的 store 类上调用
decorate函数并传递一个将值和方法映射到装饰器的对象来实现这一点:
decorate(TodoStore, {
- 我们从可观察的
todos和filter值开始:
todos: observable,
filter: observable,
- 然后,我们装饰
computed值*—*filteredTodos:
filteredTodos: computed,
- 最后但并非最不重要的是,我们装饰我们的动作:
fetch: action,
addTodo: action,
toggleTodo: action,
removeTodo: action,
filterTodos: action
})
现在,我们的 MobX 存储已经正确装饰并准备好使用!
定义 Provider 组件
现在我们可以在App组件中初始化存储,并将其传递给所有其他组件。然而,更好的做法是使用 React Context。这样,我们可以在应用程序的任何地方访问存储。MobX React 提供了一个Provider组件,它在上下文中提供存储。
现在让我们开始使用Provider组件:
- 编辑
src/index.js,并从mobx-react中导入Provider组件:
import { Provider } from 'mobx-react'
- 然后,从我们的
store.js文件中导入TodoStore:
import TodoStore from './store'
- 现在,我们创建
TodoStore类的一个新实例:
const store = new TodoStore()
- 最后,我们必须调整
ReactDOM.render()的第一个参数,以便用Provider组件包装App组件:
ReactDOM.render(
<Provider todoStore={store}>
<App />
</Provider>,
document.getElementById('root')
)
与 Redux 不同,使用 MobX 可以在我们的应用程序中提供多个存储。然而,在这里,我们只提供一个存储,并将其称为todoStore。
现在,我们的存储已经初始化并准备在所有其他组件中使用。
连接组件
现在我们的 MobX 存储作为上下文可用,我们可以开始将我们的组件连接到它。为此,MobX React 提供了inject高阶组件,我们可以用它将存储注入到我们的组件中。
在这一部分,我们将把以下组件连接到我们的 MobX 存储中:
-
App -
TodoList -
TodoItem -
AddTodo -
TodoFilter
连接 App 组件
我们将从连接我们的App组件开始,在那里我们将使用fetch动作从我们的 API 中获取所有todos。
现在让我们连接App组件:
- 编辑
src/App.js,并从mobx-react中导入inject函数:
import { inject } from 'mobx-react'
- 然后,用
inject包装App组件。inject函数用于将存储(或多个存储)作为 props 注入到组件中:
export default inject('todoStore')(function App ({ todoStore }) {
在inject函数中可以指定多个存储,如下所示:inject('todoStore', 'otherStore')。然后,将注入两个 props:todoStore和otherStore。
- 现在我们有了
todoStore,我们可以在 Effect Hook 中调用fetch动作:
useEffect(() => {
todoStore.fetch()
}, [ todoStore ])
- 现在,我们可以删除
filteredTodosMemo Hook、处理函数、StateContext.Provider组件以及我们传递给其他组件的所有 props:
return (
<div style={{ width: 400 }}>
<Header />
<AddTodo />
<hr />
<TodoList />
<hr />
<TodoFilter />
</div>
)
})
现在,我们的App组件将从 API 获取todos,然后它们将被存储在TodoStore中。
连接 TodoList 组件
在将todos存储在我们的存储中后,我们可以从存储中获取它们,然后我们可以在TodoList组件中列出所有的待办事项。
现在让我们连接TodoList组件:
- 编辑
src/TodoList.js并导入inject和observer函数:
import { inject, observer } from 'mobx-react'
-
删除所有与上下文相关的导入和 Hooks。
-
与以前一样,我们使用
inject函数来包装组件。此外,我们现在用observer函数包装我们的组件。observer函数告诉 MobX,当存储更新时,这个组件应该重新渲染:
export default inject('todoStore')(observer(function TodoList ({ todoStore }) {
- 我们现在可以使用存储中的
filteredTodos计算值,以列出所有应用了过滤器的待办事项。为了确保 MobX 仍然可以跟踪item对象发生的更改,我们在这里不使用扩展语法。如果我们使用了扩展语法,所有的待办事项都会重新渲染,即使只有一个发生了变化:
return todoStore.filteredTodos.map(item =>
<TodoItem key={item.id} item={item} />
)
}))
现在,我们的应用程序已经列出了所有的待办事项。但是,我们还不能切换或删除待办事项。
连接 TodoItem 组件
为了能够切换或删除待办事项,我们必须连接TodoItem组件。我们还将TodoItem组件定义为观察者,以便 MobX 知道它将在item对象更改时重新渲染组件。
现在让我们连接TodoItem组件:
- 编辑
src/TodoItem.js,并从mobx-react中导入inject和observer函数:
import { inject, observer } from 'mobx-react'
- 然后,用
inject和observer包装TodoItem组件:
export default inject('todoStore')(observer(function TodoItem ({ item, todoStore }) {
- 我们现在可以在组件内部使用
item对象的解构。由于它被定义为观察者,MobX 将能够在解构后跟踪item对象的更改:
const { title, completed, id } = item
- 现在我们有了
todoStore,我们可以使用它来调整我们的处理函数,并调用相应的动作:
function handleToggle () {
todoStore.toggleTodo(id)
}
function handleRemove () {
todoStore.removeTodo(id)
}
现在,我们的TodoItem组件将调用todoStore中的toggleTodo和removeTodo动作,所以我们现在可以切换和删除待办事项!
连接 AddTodo 组件
为了能够添加新的待办事项,我们必须连接AddTodo组件。
现在让我们连接AddTodo组件:
- 编辑
src/AddTodo.js并从mobx-react中导入inject函数:
import { inject } from 'mobx-react'
- 然后,用
inject包装AddTodo组件:
export default inject('todoStore')(function AddTodo ({ todoStore }) {
- 现在我们有了
todoStore,我们可以使用它来调整我们的处理函数,并调用addTodo动作:
function handleAdd () {
if (input) {
todoStore.addTodo(input)
setInput('')
}
}
现在,我们的AddTodo组件将调用我们的todoStore中的addTodo动作,所以我们现在可以添加新的待办事项!
连接 TodoFilter 组件
最后,我们必须连接TodoFilter组件,以便能够选择不同的过滤器。我们还希望显示当前选定的过滤器,因此这个组件需要是一个observer。
让我们现在连接TodoFilter组件:
- 编辑
src/TodoFilter.js并导入inject和observer函数:
import { inject, observer } from 'mobx-react'
- 我们使用
inject和observer函数来包装组件:
const TodoFilterItem = inject('todoStore')(observer(function TodoFilterItemWrapped ({ name, todoStore }) {
- 现在我们调整我们的处理函数,以调用存储中的
filterTodos动作:
function handleFilter () {
todoStore.filterTodos(name)
}
- 最后,我们调整
style对象,以使用todoStore中的filter值,以检查过滤器当前是否被选中:
const style = {
color: 'blue',
cursor: 'pointer',
fontWeight: (todoStore.filter === name) ? 'bold': 'normal'
}
- 此外,我们现在可以摆脱在
FilterItem组件中传递 props。删除以下用粗体标记的部分:
export default function TodoFilter (props) {
return ( <div> <TodoFilterItem {...props} name="all" />{' / '} <TodoFilterItem {...props} name="active" />{' / '} <TodoFilterItem {...props} name="completed" /> </div> ) }
现在,我们可以选择新的过滤器,它们将被标记为选定,并以粗体显示。待办事项列表也将自动过滤,因为 MobX 检测到filter值的变化,导致filteredTodos计算值更新,并且TodoList观察者组件重新渲染。
示例代码
示例代码可以在Chapter13/chapter13_1文件夹中找到。
只需运行npm install以安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。
使用 MobX 和 Hooks
在上一节中,我们学习了如何在 React 中使用 MobX。正如我们所见,为了能够将我们的组件连接到 MobX 存储,我们需要使用inject函数将它们包装起来,并且在某些情况下,还需要使用observer函数。自从mobx-react的 v6 版本发布以来,我们可以使用 Hooks 来连接我们的组件到 MobX 存储,而不是使用这些高阶组件来包装我们的组件。我们现在要使用 MobX 和 Hooks!
定义一个存储 Hook
首先,我们必须定义一个 Hook 以便访问我们自己的存储。正如我们之前学到的,MobX 使用 React Context 来提供和注入状态到各种组件中。我们可以从mobx-react中获取MobXProviderContext并创建我们自己的自定义上下文 Hook 以便访问所有存储。然后,我们可以创建另一个 Hook,专门访问我们的TodoStore。
所以,让我们开始定义一个存储 Hook:
-
创建一个新的
src/hooks.js文件。 -
从
react中导入useContextHook,以及从mobx-react中导入MobXProviderContext:
import { useContext } from 'react'
import { MobXProviderContext } from 'mobx-react'
- 现在,我们定义并导出一个
useStoresHook,它返回一个用于MobXProviderContext的 Context Hook:
export function useStores () {
return useContext(MobXProviderContext)
}
- 最后,我们定义一个
useTodoStoreHook,它从我们之前的 Hook 中获取todoStore,然后返回它:
export function useTodoStore () {
const { todoStore } = useStores()
return todoStore
}
现在,我们有一个通用的 Hook,可以访问 MobX 的所有 stores,以及一个特定的 Hook 来访问TodoStore。如果需要的话,我们也可以在以后定义更多的 Hooks 来访问其他 stores。
升级组件到 Hooks
创建一个 Hook 来访问我们的 store 后,我们可以使用它来代替用inject高阶组件函数包装我们的组件。在接下来的部分中,我们将看到如何使用 Hooks 来升级我们的各种组件。
为 App 组件使用 Hooks
我们将从升级我们的App组件开始。逐渐重构组件,使其使用 Hooks 是可能的。我们不需要一次性重构每个组件。
现在让我们为App组件使用 Hooks:
- 编辑
src/App.js并删除以下import语句:
import { inject } from 'mobx-react'
- 然后,从我们的
hooks.js文件中导入useTodoStoreHook:
import { useTodoStore } from './hooks'
- 现在,删除包装
App组件的inject函数,并删除所有 props。App函数定义现在应该如下所示:
export default function App () {
- 最后,使用我们的 Todo Store Hook 来获取
todoStore对象:
const todoStore = useTodoStore()
如你所见,我们的应用程序仍然以与以前相同的方式工作!然而,我们现在在App组件中使用 Hooks,这使得代码更加清晰和简洁。
为TodoList组件使用 Hooks
接下来,我们将升级我们的TodoList组件。此外,我们还将使用useObserver Hook,它替换了observer高阶组件。
现在让我们为TodoList组件使用 Hooks:
- 编辑
src/TodoList.js,并删除以下导入语句:
import { inject, observer } from 'mobx-react'
- 然后,从
mobx-react中导入useObserverHook,以及从我们的hooks.js文件中导入useTodoStoreHook:
import { useObserver } from 'mobx-react'
import { useTodoStore } from './hooks'
- 现在,删除包装
TodoList组件的inject和observer函数,并且也删除所有 props。TodoList函数定义现在应该如下所示:
export default function TodoList () {
- 再次,我们使用 Todo Store Hook 来获取
todoStore对象:
const todoStore = useTodoStore()
- 最后,我们用
useObserverHook 包装返回的元素。在 Observer Hook 中的所有内容将在 Hook 中使用的状态发生变化时重新计算:
return useObserver(() =>
todoStore.filteredTodos.map(item =>
<TodoItem key={item.id} item={item} />
)
)
}
在我们的情况下,MobX 将检测到通过useObserver Hook 定义的观察者依赖于todoStore.filteredTodos,而filteredTodos依赖于filter和todos值。因此,每当filter值或todos数组发生更改时,列表将重新渲染。
为TodoItem组件使用 Hooks
接下来,我们将升级TodoItem组件,这将是与TodoList组件相似的过程。
现在让我们为TodoItem组件使用 Hooks:
- 编辑
src/TodoItem.js并删除以下import语句:
import { inject, observer } from 'mobx-react'
- 然后,从
mobx-react中导入useObserverHook,从我们的hooks.js文件中导入useTodoStoreHook:
import { useObserver } from 'mobx-react'
import { useTodoStore } from './hooks'
- 现在,删除包裹
TodoItem组件的inject和observer函数,也删除todoStore属性。TodoItem函数定义现在应该如下所示:
export default function TodoItem ({ item }) {
- 接下来,我们必须删除解构(粗体代码),因为我们整个组件不再被定义为可观察的,因此 MobX 将无法跟踪
item对象的更改:
const { title, completed, id } = item
- 然后,使用 Todo Store Hook 来获取
todoStore对象:
const todoStore = useTodoStore()
- 现在,我们必须调整处理函数,使其直接使用
item.id而不是id。请注意,我们假设id不会改变,因此它不会被包裹在 Observer Hook 中:
function handleToggle () {
todoStore.toggleTodo(item.id)
}
function handleRemove () {
todoStore.removeTodo(item.id)
}
- 最后,我们用 Observer Hook 包裹
return语句并在那里进行解构。这确保了 MobX 会跟踪item对象的更改,并且当对象的属性更改时,组件将相应地重新渲染:
return useObserver(() => {
const { title, completed } = item
return (
<div style={{ width: 400, height: 25 }}>
<input type="checkbox" checked={completed} onChange={handleToggle} />
{title}
<button style={{ float: 'right' }} onClick={handleRemove}>x</button>
</div>
)
})
}
现在,我们的TodoItem组件已经正确连接到 MobX 存储。
如果item.id属性发生更改,我们将不得不将处理函数和return函数包裹在单个useObserver Hook 中,如下所示:
return useObserver(() => {
const { title, completed, id } = item
function handleToggle () {
todoStore.toggleTodo(id)
}
function handleRemove () {
todoStore.removeTodo(id)
}
return (
<div style={{ width: 400, height: 25 }}>
<input type="checkbox" checked={completed} onChange={handleToggle} />
{title}
<button style={{ float: 'right' }} onClick={handleRemove}>x</button>
</div>
)
})
请注意,我们不能将处理函数和return语句分别包裹在单独的 Observer Hooks 中,因为这样处理函数将只在第一个 Observer Hook 的闭包内定义。这意味着我们将无法从第二个 Observer Hook 内访问处理函数。
接下来,我们将继续通过使用 Hooks 来升级AddTodo组件的组件。
为AddTodo组件使用 Hooks
我们重复与App组件中相同的升级过程,用于AddTodo组件,如下所示:
- 编辑
src/AddTodo.js并删除以下import语句:
import { inject } from 'mobx-react'
- 然后,从我们的
hooks.js文件中导入useTodoStoreHook:
import { useTodoStore } from './hooks'
- 现在,删除包装
AddTodo组件的inject函数,也删除所有 props。AddTodo函数定义现在应该如下所示:
export default function AddTodo () {
- 最后,使用 Todo Store Hook 来获取
todoStore对象:
const todoStore = useTodoStore()
现在,我们的AddTodo组件已连接到 MobX 存储,我们可以继续升级TodoFilter组件。
使用 Hooks 来处理TodoFilter组件
对于TodoFilter组件,我们将使用类似于我们用于TodoList组件的过程。我们将使用我们的useTodoStore Hook 和useObserver Hook。
现在让我们为TodoFilter组件使用 Hooks:
- 编辑
src/TodoFilter.js并删除以下import语句:
import { inject, observer } from 'mobx-react'
- 然后,从
mobx-react导入useObserverHook,以及从我们的hooks.js文件中导入useTodoStoreHook:
import { useObserver } from 'mobx-react'
import { useTodoStore } from './hooks'
- 现在,删除包装
TodoFilterItem组件的inject和observer函数,也删除todoStoreprop。TodoFilterItem函数定义现在应该如下所示:
function TodoFilterItem ({ name }) {
- 再次,我们使用 Todo Store Hook 来获取
todoStore对象:
const todoStore = useTodoStore()
- 最后,我们使用
useObserverHook 将style对象包装起来。请记住,Observer Hook 内的所有内容都将在 Hook 中使用的状态发生变化时重新计算:
const style = useObserver(() => ({
color: 'blue',
cursor: 'pointer',
fontWeight: (todoStore.filter === name) ? 'bold' : 'normal'
}))
在这种情况下,当todoStore.filter值发生变化时,style对象将被重新计算,这将导致元素重新渲染,并在选择不同的过滤器时更改字体加粗。
示例代码
示例代码可以在Chapter13/chapter13_2文件夹中找到。
只需运行npm install以安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。
使用本地存储 Hook
除了提供全局存储以存储应用程序范围的状态外,MobX 还提供了本地存储以存储本地状态。要创建本地存储,我们可以使用useLocalStore Hook。
现在,我们将在AddTodo组件中实现 Local Store Hook:
- 编辑
src/AddTodo.js并导入useLocalStoreHook,以及从mobx-react导入useObserverHook:
import { useLocalStore, useObserver } from 'mobx-react'
- 然后,删除以下 State Hook:
const [ input, setInput ] = useState('')
用本地存储 Hook 替换它:
const inputStore = useLocalStore(() => ({
在这个本地存储中,我们可以定义状态值、计算值和动作。useLocalStore Hook 将自动将值装饰为可观察的,getter 函数(get前缀)作为计算值,普通函数作为动作。
- 我们从
input字段的value状态开始:
value: '',
- 然后,我们定义一个计算值,它将告诉我们添加按钮是否应该被
disabled:
get disabled () {
return !this.value
},
- 接下来,我们定义动作。第一个动作从输入事件更新
value:
updateFromInput (e) {
this.value = e.target.value
},
- 然后,我们定义另一个动作来更新
value,从一个简单的字符串:
update (val) {
this.value = val
}
}))
- 现在,我们可以调整输入处理函数,并调用
updateFromInput动作:
function handleInput (e) {
inputStore.updateFromInput(e)
}
- 我们还需要调整
handleAdd函数:
function handleAdd () {
if (inputStore.value) {
todoStore.addTodo(inputStore.value)
inputStore.update('') }}
- 最后,我们用
useObserverHook 包装元素,以确保input字段的值在更改时得到更新,并调整disabled和value属性:
return useObserver(() => (
<form onSubmit={e => { e.preventDefault(); handleAdd() }}>
<input
type="text"
placeholder="enter new task..."
style={{ width: 350, height: 15 }}
value={inputStore.value}
onChange={handleInput}
/>
<input
type="submit"
style={{ float: 'right', marginTop: 2 }}
disabled={inputStore.disabled}
value="add"
/>
</form>
))
}
现在,我们的AddTodo组件使用一个本地 MobX 存储来处理其输入值,并禁用/启用按钮。如你所见,使用 MobX,可以使用多个存储,用于本地和全局状态。难点在于决定如何分割和分组你的存储,以使其对给定应用程序有意义。
示例代码
示例代码可以在Chapter13/chapter13_3文件夹中找到。
只需运行npm install来安装所有依赖项,然后运行npm start启动应用程序,然后在浏览器中访问http://localhost:3000(如果没有自动打开)。
迁移 MobX 应用程序
在上一节中,我们学习了如何用 Hooks 替换现有 MobX 应用程序中的 MobX 高阶组件,如inject和observer。现在,我们将学习如何将现有 MobX 应用程序中的本地状态迁移到 Hooks 中。
通过以下三个步骤,可以将现有的 MobX 应用程序迁移到基于 Hook 的解决方案:
-
使用 State Hook 处理简单的本地状态
-
使用
useLocalStateHook 处理复杂的本地状态 -
将全局状态保留在单独的 MobX 存储中
我们已经学习了如何在本书的早期章节中使用 State Hook。State Hooks 对于简单的状态,比如复选框的当前状态,是有意义的。
我们已经在本章中学习了如何使用useLocalState Hook。我们可以使用本地状态 Hook 处理复杂的本地状态,比如多个字段相互交互的复杂表单。然后,我们可以用单个本地状态 Hook 替换多个 State 和 Effect Hooks 以及计算值和动作。
最后,全局状态应该存储在单独的 MobX 存储中,例如在本章中定义的TodoStore。在 MobX 中,可以创建多个存储并使用Provider组件传递给组件。然后我们可以为每个存储创建一个单独的自定义 Hook。
MobX 的权衡取舍
总之,让我们总结在 Web 应用程序中使用 MobX 的利弊。首先,让我们从积极方面开始:
-
它提供了一种简单的处理状态变化的方式
-
需要更少的样板代码
-
它提供了灵活性,可以结构化我们的应用程序代码
-
可以使用多个全局和本地存储
-
它使
App组件更简单(它将状态管理和操作转移到 MobX)
MobX 非常适合处理复杂状态变化和在许多组件中使用的状态的小型和大型项目。
然而,使用 MobX 也有缺点:
-
状态变化可能发生在任何地方,不仅仅是在单个存储中
-
它的灵活性意味着可能以不好的方式构建项目,这可能会导致错误或缺陷
-
MobX 需要一个包装组件(
Provider)来连接应用程序到存储,如果我们想要获得所有功能(我们可以直接导入和使用 MobX 存储,但这将破坏诸如服务器端渲染之类的功能)
如果状态变化很简单,并且只需要组件内部的本地状态,就不应该使用 MobX。在这种情况下,状态或 Reducer Hook 可能足够了。使用 Reducer 和 State Hooks,我们不需要包装组件来连接我们的应用程序到存储。
灵活性是一件好事,但它也可能导致我们不好地构建项目。然而,MobX 提供了一个名为mobx-state-tree的项目,它允许我们使我们的 MobX 应用程序更有结构并强制执行某种类型的架构。更多信息可以在以下 GitHub 存储库的项目页面中找到:github.com/mobxjs/mobx-state-tree。
总结
在本章中,我们首先学习了 MobX 是什么,它由哪些元素组成,以及它们如何一起工作。然后,我们学习了如何在实践中使用 MobX 进行状态管理。我们还学习了如何将 MobX 存储与 React 组件连接起来,使用inject和observer高阶组件。接下来,我们用 Hooks 替换了高阶组件,使我们的代码更加清晰简洁。我们还学习了如何使用 Local Store Hook 处理 MobX 中复杂的本地状态。最后,我们学习了如何将现有的 MobX 应用迁移到 Hooks,并总结了使用 MobX 的权衡。
本章标志着本书的结束。在本书中,我们从动机开始使用 Hooks。我们了解到在 React 应用中有一些常见问题,如果没有 Hooks,很难解决。然后,我们使用 Hooks 创建了我们的第一个组件,并将其与基于类的组件解决方案进行了比较。接下来,我们深入学习了各种 Hooks,从最常见的 State Hook 开始。我们还学习了如何使用 Hooks 解决常见问题,例如条件性 Hooks 和循环中的 Hooks。
在深入学习了 State Hook 之后,我们使用 Hooks 开发了一个小型博客应用。然后,我们学习了 Reducer Hooks、Effect Hooks 和 Context Hooks,以便能够在我们的应用中实现更多功能。接下来,我们学习了如何使用 Hooks 有效地请求资源。此外,我们学习了如何使用React.memo防止不必要的重新渲染,以及如何使用 React Suspense 实现延迟加载。然后,我们在我们的博客应用中实现了路由,并学习了 Hooks 如何使动态路由变得更加容易。
我们还学习了社区提供的各种 Hooks,这些 Hooks 使处理输入字段、各种数据结构、响应式设计和撤销/重做功能变得更加容易。此外,我们学习了 Hooks 的规则,如何创建我们自己的自定义 Hooks,以及 Hooks 之间的交互方式。最后,我们学习了如何有效地从现有的基于类的应用迁移到基于 Hooks 的解决方案。最后,我们学习了如何将 Hooks 与 Redux 和 MobX 一起使用,以及如何将现有的 Redux 和 MobX 应用迁移到 Hooks。
现在我们已经深入了解了 Hooks,我们准备在我们的应用程序中使用它们!我们还学会了如何将现有项目迁移到 Hooks,所以我们现在可以开始做这个。我希望你喜欢学习 React Hooks,并且期待在你的应用程序中实现 Hooks!我相信使用 Hooks 会让编码对你来说更加愉快,就像对我一样。
问题
为了回顾我们在本章学到的内容,请尝试回答以下问题:
-
哪些元素构成 MobX 生命周期?
-
MobX 提供哪些装饰器?
-
我们如何将组件连接到 MobX?
-
MobX 提供哪些 Hooks?
-
我们如何使用 Hooks 访问 MobX 存储?
-
我们可以使用 MobX 存储本地状态吗?
-
我们应该如何将现有的 MobX 应用程序迁移到 Hooks?
-
使用 MobX 的优点是什么?
-
使用 MobX 有哪些缺点?
-
何时不应该使用 MobX?
进一步阅读
如果您对本章学习的概念更多信息感兴趣,请查看以下阅读材料:
-
来自官方 MobX 文档的 MobX 简介:
mobx.js.org/getting-started.html -
官方 MobX 文档:
mobx.js.org -
关于 MobX 基础知识的视频课程:
egghead.io/lessons/react-sync-the-ui-with-the-app-state-using-mobx-observable-and-observer-in-react -
官方 MobX React 文档:
mobx-react.js.org/ -
GitHub 上的
mobx项目:github.com/mobxjs/mobx -
GitHub 上的
mobx-react项目:github.com/mobxjs/mobx-react -
GitHub 上的
mobx-state-tree项目:github.com/mobxjs/mobx-state-tree
第十四章:评估
问题的答案
在这里,我们回答每章末尾提出的所有问题。您可以使用这些问题来回顾您在整本书中学到的知识。
第一章:介绍 React 和 React Hooks
- React 的三个基本原则是什么?
-
声明性的: 我们告诉 React 我们想要什么,而不是告诉 React 如何做事。因此,我们可以轻松设计我们的应用程序,当数据发生变化时,React 将高效地更新和渲染恰当的组件。
-
基于组件的: React 封装了管理自己状态和视图的组件,然后允许我们组合它们以创建复杂的用户界面。
-
学一次,随处编写: React 不对您的技术堆栈做出假设,并努力确保您可以尽可能少地重写现有代码来开发。
- React 中有哪两种类型的组件?
-
函数组件: 接受 props 作为参数并返回用户界面的 JavaScript 函数(通常通过 JSX)
-
类组件: 提供
render方法的 JavaScript 类,该方法返回用户界面(通常通过 JSX)
- 在 React 中类组件有哪些问题?
-
JavaScript 类对开发人员来说也很难理解:
this上下文可能令人困惑,有时我们不得不同时在多个地方编写代码。 -
它们对机器也很难理解:很难说哪些方法会被调用,因此性能优化实际上并不可行。
-
- 它们不是声明性的,因此违反了 React 的基本原则:要使用 React 功能,我们必须编写代码告诉 React 要做什么,而不是如何做。
- 在 React 中使用高阶组件的问题是什么?
- 使用高阶组件会向我们的视图树引入实际上在视图结构方面并不重要的组件。拥有许多高阶组件会导致所谓的包装器地狱。
- 我们可以使用哪个工具来设置 React 项目,我们需要运行什么命令来使用它?
- 我们可以使用
create-react-app。要创建一个新项目,我们必须运行npx create-react-app <app-name>或yarn create react-app <app-name>。
- 如果我们在类组件中遇到以下错误,我们需要做什么:TypeError: undefined is not an object (evaluating 'this.setState')?
- 我们忘记在类的
constructor中重新绑定方法的this上下文。结果,this指向的不是类,而是输入字段的上下文。
- 我们如何使用 Hooks 访问和设置 React 状态?
- 我们使用
useState()Hook 如下:const [ name, setName ] = useState('')
- 与类组件相比,使用带有 Hooks 的函数组件的优势是什么?
- 具有 Hooks 的函数组件不会遇到与类相同的问题。它们是声明性的,因此更适合 React 的基本原则。Hooks 还使我们的代码更简洁,更易于理解。
- 在更新 React 时,我们是否需要用 Hooks 替换所有类组件?
- 不,我们不需要替换所有类组件。带有 Hooks 的函数组件可以与现有的类组件并存,并且是 100%向后兼容的。我们可以简单地使用 Hooks 编写新组件,或者以自己的步调升级现有组件。
- React 提供的三个基本 Hooks 是什么?
useState,useEffect和useContextHooks 是 React 提供的基本 Hooks,并在项目中经常使用。但是,React 还提供了一些更高级的 Hooks。
第二章:使用 State Hook
- 在开发自己的
useStatehook 的重新实现时,我们遇到了什么问题?我们是如何解决这些问题的?
-
一个问题是每次组件被渲染时值的初始化。我们通过使用全局变量来存储值来解决了这个问题。
-
然后,我们遇到了多个 Hooks 写入同一个全局变量的问题。为了解决这个问题,我们将值存储在数组中,并通过为每个 Hook 分配索引来跟踪当前 Hook。
- 为什么在 React 的 Hooks 实现中不可能有条件 Hooks?
- 条件 Hooks 是不可能的,因为 React 使用 Hook 定义的顺序来跟踪值。如果我们稍后更改 Hooks 的顺序,值将分配给不同的 Hooks。
- Hooks 是什么,它们处理什么?
- Hooks 是处理 React 应用程序中状态和效果的函数
- 在使用 Hooks 时,我们需要注意什么?
- 我们需要确保 Hooks 的顺序始终保持不变,因此我们不能在循环或条件语句中使用 Hooks
- Hooks 的替代 API 想法的常见问题是什么?
-
命名钩子存在名称冲突的问题。即使在库中使用钩子时,每个钩子也必须具有唯一的名称。
-
钩子工厂需要更多的样板代码,主要是实例化每个钩子两次,一次在组件外部,一次在组件内部。此外,它们使得创建自定义钩子变得更加困难。
- 我们如何实现条件钩子?
- 在简单情况下,我们总是可以定义钩子。否则,我们必须拆分组件,并有条件地渲染一个单独的组件,而不是有条件地渲染钩子。
- 我们如何在循环中实现钩子?
- 在简单情况下,我们可以将数组存储在状态钩子中。否则,我们必须拆分组件并在循环中渲染一个单独的组件。
第三章:使用 React Hooks 编写您的第一个应用程序
- 在 React 中,文件结构的最佳实践是什么?
- 首先从一个简单的结构开始,需要时再进行更深层次的嵌套。在启动项目时,不要花太多时间考虑文件结构。
- 在拆分 React 组件时应该使用哪个原则?
- 单一职责原则,即每个组件应对功能的单个封装部分负责
map函数是做什么的?
map函数将给定的函数应用于数组的所有元素,并返回具有结果的新数组
- 解构是如何工作的,我们什么时候使用它?
- 通过解构,我们可以通过在赋值的左侧指定结构和变量名称来从对象中获取属性或从数组中获取元素。我们可以使用解构来获取 React 组件中的某些 props。
- 扩展运算符是如何工作的,我们什么时候使用它?
- 扩展运算符在另一个对象/数组的特定点插入对象的所有属性或数组的所有元素。它可以用于创建新的数组或对象,或者将对象的所有属性作为 props 传递给 React 组件。
- 我们如何使用 React Hooks 处理输入字段?
- 我们为输入字段值创建一个状态钩子,并定义一个设置值的处理函数
- 本地状态钩子应该在哪里定义?
- 本地状态钩子应始终在使用它们的组件中定义
- 什么是全局状态?
- 全局状态是在整个应用程序中跨多个组件使用的状态
- 全局状态钩子应该在哪里定义?
- 全局状态 Hook 应该尽可能高地定义在组件树中。在我们的例子中,我们在
App组件中定义了它们。
第四章:使用 Reducer 和 Effect Hooks
- State Hook 的常见问题是什么?
- 使用 State Hook 很难进行复杂的状态改变
- 什么是动作?
- 动作是描述状态改变的对象,例如,
{ type: 'CHANGE_FILTER', byAuthor: 'Daniel Bugl' }
- 什么是 reducer?
- Reducer 是处理状态改变的函数。它们接受当前状态和一个动作对象,并返回一个新状态。
- 何时应该使用 Reducer Hook 而不是 State Hook?
-
当需要复杂的状态改变时,应该使用 Reducer Hook。通常,这适用于全局状态。
-
当多个 State Hook 的 setter 函数一起被调用时,这是使用 Reducer Hook 的一个很好的指标。
- 为了将 State Hook 转换为 Reducer Hook,需要哪些步骤?
- 我们首先需要定义动作,然后是 reducer 函数,最后是 Reducer Hook
- 我们如何更容易地创建动作?
- 我们可以定义返回动作对象的函数,称为动作创建者
- 何时应该合并 Reducer Hook?
- 当我们想要避免有两个单独的 dispatch 函数或者同一个动作修改多个 reducer 中的状态时
- 在合并 Reducer Hook 时需要注意什么?
- 我们需要确保每个 reducer 对于未处理的动作返回当前状态
- 在类组件中,Effect Hook 的等价物是什么?
- 在 React 类组件中,我们将使用
componentDidMount和componentDidUpdate来处理效果
- 使用 Effect Hook 相对于类组件有什么优势?
- 使用 Effect Hook 时,我们不需要同时定义
componentDidMount和componentDidUpdate。此外,Effect Hook 更容易理解,我们不需要知道 React 内部的工作原理就能使用它们。
第五章:实现 React 上下文
- 上下文避免了哪些问题?
- 上下文避免了必须通过多个组件层级传递 props 的问题
- 上下文由哪两部分组成?
- React 上下文由提供者和消费者组成
- 使用上下文是否需要两部分都定义?
- 提供者不是必需的,因为当没有定义提供者时,上下文将使用传递给
React.createContext的默认值
- 使用 Hooks 而不是传统上下文消费者的优势是什么?
- Hooks 不需要使用组件和渲染 props 来进行消费。使用多个上下文和消费者组件会使我们的组件树变得非常深,使我们的应用程序更难调试和维护。Hooks 通过允许我们通过简单调用 Hook 函数来消费上下文来避免这个问题。
- 上下文的替代方案是什么,什么时候应该使用它?
- 上下文使得重用组件变得更加困难。只有在我们需要在不同嵌套级别的多个组件中访问数据时,才应该使用上下文。否则,我们可以通过传递 props 或传递渲染的组件来使用控制反转技术。
- 我们如何实现动态更改上下文?
- 我们需要使用 State Hook 来为上下文提供值
- 什么时候使用上下文来管理状态是有意义的?
- 通常情况下,使用上下文来管理全局状态是有意义的,这些状态在不同嵌套级别的多个组件中使用。
第六章:实现请求和 React Suspense
- 如何从一个简单的 JSON 文件中轻松创建一个完整的 REST API?
- 我们可以使用
json-server工具从 JSON 文件创建一个完整的 REST API,用于开发和测试
- 在开发过程中使用代理访问后端服务器的优势是什么?
- 在使用代理时,在开发过程中我们不需要处理跨站点限制
- 我们可以使用哪些组合的 Hooks 来实现请求?
- 我们可以使用 Effect 和 State 或 Reducer Hook 来实现请求
- 我们可以使用哪些库来实现请求?
- 我们还可以使用
axios和react-request-hook库来实现请求
- 我们如何使用
react-request-hook处理加载状态?
- 我们可以使用从
useResourceHook 返回的result.isLoading标志,并有条件地显示加载消息
- 我们如何使用
react-request-hook处理错误?
- 我们可以使用从
useResourceHook 返回的result.error对象并分派错误操作
- 如何防止组件不必要的重新渲染?
- 使用
React.memo,我们可以防止不必要的重新渲染,类似于shouldComponentUpdate
- 我们如何减少应用程序的捆绑大小?
- 我们可以使用
React.Suspense来延迟加载某些组件,这意味着只有在需要时才会从服务器请求它们。
第七章:使用 Hooks 进行路由
- 为什么我们需要定义单独的页面?
- 大多数大型应用程序由多个页面组成。例如,每篇博客文章都有一个单独的页面
- 我们如何使用 Navi 库定义路由?
- 我们使用
mount函数并传递一个将路径映射到route函数的对象
- 我们如何使用 URL 参数定义路由?
- 我们可以使用
:parameter语法在路径中指定 URL 参数
- 如何使用 Navi 定义静态链接?
- 可以使用
react-navi中的Link组件来定义静态链接
- 我们如何实现动态导航?
- 可以使用
useNavigationHook 并调用navigation.navigate()来实现动态导航
- 用于访问当前路由信息的 Hook 是什么?
useCurrentRouteHook 为我们提供了关于当前路由的所有信息
- 用于访问当前加载路由的 Hook 是什么?
useLoadingRouteHook 为我们提供了关于当前正在加载的路由的所有信息
第八章:使用社区 Hooks
- 我们可以使用哪个 Hook 来简化输入字段处理?
- 我们可以使用
react-hookedup库中的useInputHook
- 如何使用 Effect Hooks 实现
componentDidMount和componentWillUnmount生命周期?
-
componentDidMount可以通过使用 Effect Hook 并将空数组作为第二个参数传递来实现。例如,useEffect(() => console.log('did mount'), [])。 -
componentWillUnmount可以通过从 Effect Hook 返回一个函数来实现,其中空数组作为第二个参数传递,例如,useEffect(() => { return () => console.log('will unmount') }, [])。
- 我们如何使用 Hooks 来获得
this.setState()的行为?
this.setState()将现有状态对象与给定状态对象合并。我们可以使用useMergeStateHook 来获得相同的行为,而不是简单的 State Hook。
- 为什么我们应该使用定时器 Hooks 而不是直接调用
setTimeout和setInterval?
- 在定义简单的超时或间隔时,当组件重新渲染时它们将被重置。为了防止这种重置发生,我们必须使用
react-hookedup中的useTimeout和useIntervalHook。
- 我们可以使用哪些 Hooks 来简化处理常见数据结构?
- 我们可以使用
react-hookedup中的useBoolean、useArray和useCounterHooks
- 何时应该使用 Hooks 进行响应式设计,而不是简单地使用 CSS 媒体查询?
- 当在画布或 WebGL 中渲染元素时,或者当我们动态地想要根据窗口大小决定是否加载组件时,我们应该使用 Hooks 进行响应式设计
- 我们可以使用哪个 Hook 来实现撤销/重做功能?
- 我们可以使用
use-undo库中的useUndoHook 来在我们的应用程序中实现简单的撤销/重做功能
- 什么是防抖动?为什么我们需要这样做?
- 防抖动意味着函数只会在一定时间后被调用,而不是每次事件触发时都被调用。使用防抖动,我们可以在文本字段中输入的值仅在每秒后存储在撤销历史中,而不是在每次输入字符后。
- 我们可以使用哪个 Hook 来进行防抖动?
- 我们可以使用
use-debounce库中的useDebounce或useDebouncedCallbackHook
第九章:Hook 的规则
- Hook 可以在哪里调用?
- Hook 只能在 React 函数组件或自定义 Hook 的开头调用
- 我们可以在 React 类组件中使用 Hook 吗?
- 不,不可能在 React 类组件中使用 Hook
- 关于 Hook 的顺序,我们需要注意什么?
- Hook 的顺序不应该改变,因为它用于跟踪各种 Hook 的值
- Hook 可以在条件语句、循环或嵌套函数中调用吗?
- 不,Hook 不能在条件语句、循环或嵌套函数中调用,因为那样会改变 Hook 的顺序
- Hook 的命名约定是什么?
- Hook 函数名称应始终以
use前缀开头,然后是CamelCase中的名称。例如:useSomeHookName。
- 我们如何自动强制执行 Hook 的规则?
- 我们可以使用
eslint和eslint-plugin-react-hooks来强制执行 Hook 的规则
- 详尽的依赖规则是什么?
- 详尽的依赖规则确保在 Effect Hook 中使用的所有变量都通过第二个参数列为依赖项
- 我们如何自动修复 linter 警告?
- 我们可以运行
npm run lint -- --fix命令来自动修复 linter 警告。例如,运行此命令将自动将 Effect Hook 中使用的所有变量输入为依赖项。
第十章:构建自己的 Hooks
- 我们如何从现有代码中提取自定义 Hook?
- 我们可以简单地将我们的代码放入一个单独的函数中。在自定义 Hook 函数中可以使用其他 Hook 函数,但我们需要确保不违反 Hook 的规则。
- 创建 API Hooks 的优势是什么?
- 当为各种 API 调用定义单独的函数时,如果 API 以后发生更改,我们可以很容易地调整它们,因为我们把所有与 API 相关的代码放在一个地方
- 我们何时应该将功能提取到自定义 Hook 中?
- 当某个功能在多个地方使用或以后可能被重复使用时,我们应该创建一个自定义 Hook
- 我们如何使用自定义 Hooks?
- 我们可以像调用官方 React Hooks 或来自库的 Hooks 一样简单地调用自定义 Hooks
- 何时应该创建本地 Hooks?
- 当我们想要将某个功能封装在单独的函数中,但它只会在单个组件中使用时,可以使用本地 Hooks
- Hooks 之间有哪些交互?
- 我们可以在 Hook 函数中使用其他 Hooks,并且可以从其他 Hooks 传递值到 Hooks
- 我们可以使用哪个库来测试 Hooks?
- 我们可以使用
jest测试运行器与 React Hooks 测试库(@testing-library/react-hooks)和react-test-renderer来测试 Hooks
- 我们如何测试 Hook 动作?
- 可以使用
act函数来测试 Hook 动作。例如,act(() => result.current.increment())。
- 我们如何测试上下文?
- 通过编写一个上下文包装函数来测试上下文,该函数返回提供者。然后可以将包装函数传递给
renderHook函数。例如,const { result } = renderHook(() => useTheme(), { wrapper: ThemeContextWrapper })。
- 我们如何测试异步代码?
- 我们可以使用与从
renderHook返回的waitForNextUpdate函数结合使用 async/await 构造来等待异步代码完成运行
第十一章:从 React 类组件迁移
- React 类组件是如何定义的?
- React 类组件是通过使用
class ComponentName extends React.Component {来定义的
- 在使用类组件的
constructor时,我们需要调用什么?为什么?
- 我们首先需要调用
super(props)来确保 props 被传递给React.Component类
- 我们如何在类组件中设置初始状态?
- 我们可以通过在
constructor中定义this.state对象来在类组件中设置初始状态
- 我们如何改变类组件的状态?
- 在类组件中,我们使用
this.setState()来改变状态
- 为什么我们需要重新绑定类组件方法的
this上下文?
- 当将方法传递给元素作为事件处理程序时,
this上下文会更改为触发事件的元素。我们需要重新绑定this上下文到类以防止这种情况发生。
- 我们如何重新绑定
this上下文?
- 我们需要在构造函数中对方法使用
.bind(this)。例如,this.handleInput = this.handleInput.bind(this)。
- 如何在类组件中使用 React 上下文?
-
我们可以设置
contextType,然后访问this.context。例如,static contextType = StateContext。 -
如果我们想使用多个上下文,我们可以使用上下文消费者。例如,
<StateContext.Consumer>{value => <div>State is: {value}</div>}</StateContext.Consumer>。
- 在迁移到 Hooks 时,我们可以用什么替换状态管理?
- 我们可以用 State Hook 替换
this.state和this.setState。
- 使用 Hooks 与类组件相比有哪些权衡?
-
使用 Hooks 的函数组件更简单(不需要处理构造函数、
this或多次解构相同的值,处理上下文、props 和状态时没有魔法,不需要同时定义componentDidMount和componentDidUpdate)。函数组件还鼓励创建小而简单的组件,更容易重构和测试,需要更少的代码,对初学者更容易理解,更具有声明性。 -
然而,当遵循特定约定并使用最新的 JavaScript 特性来避免
this重新绑定时,类组件可能是可以的。此外,由于具有现有知识,类组件可能更容易为团队理解。
- 何时以及如何将现有项目迁移到 Hooks?
- 在适当的时候逐步用基于 Hook 的函数组件替换旧的类组件。例如,当您已经在重构一个组件时。
第十二章:Redux 和 Hooks
- Redux 应该用于哪种状态?
- Redux 应该用于全局状态,即在我们的应用程序中多个组件中使用的状态。
- Redux 由哪些元素组成?
- Redux 由store(描述应用程序完整状态的对象)、actions(描述状态修改的对象)、action creators(创建操作对象的函数)、reducers(接受当前状态和操作对象并返回新状态的函数)和connectors(将现有组件连接到 Redux 的高阶组件)组成
- Redux 的三个原则是什么?
-
单一数据源(数据应始终具有单一来源)
-
只读状态(不可能直接修改状态,只能通过分派操作来修改)
-
状态更改通过纯函数处理(给定相同的状态和操作,reducer 将始终返回相同的新状态)
- 为什么要定义操作类型?
- 动作类型避免在定义或比较动作的
type属性时出现拼写错误。
- 我们如何将组件连接到 Redux?
- 我们可以使用
connect高阶组件,也可以使用 Dispatch 和 Selector Hooks。
- 我们可以使用哪些 Hooks 与 Redux 一起使用?
- 使用
useDispatch来获取分发函数,使用useSelector来获取状态的某个部分,使用useStore来获取 Redux 存储(用于特殊用例,比如替换 reducers)。
- 为什么我们应该创建可重用的选择器?
- 可重用的选择器可以在多个组件中使用。此外,它们会记忆结果,并且只在状态改变时重新计算。
- 我们如何迁移 Redux 应用程序?
- 我们应该首先用 State Hooks 替换简单的本地状态,比如输入字段的值。然后用 Reducer Hooks 替换复杂的本地状态。我们将全局状态(在多个组件中使用)保留在 Redux 存储中。最后,我们使用 Selector 和 Dispatch Hooks 来代替
connect高阶组件。
- Redux 的权衡是什么?
-
使用 Redux 的优点是:它提供了一定的项目结构,使我们可以轻松地扩展和修改代码,代码中的错误可能性较少,它的性能比仅使用 React 上下文来管理状态要好,并且通过将状态管理和动作创建者转移到 Redux,使我们的
App组件变得更简单。 -
使用 Redux 的缺点是:它需要大量样板代码,项目结构变得更加复杂,并且需要一个包装组件(
Provider)来连接应用程序到存储。
- 我们何时应该使用 Redux?
- 我们应该只在需要复杂状态更改的应用程序中使用 Redux。对于简单项目,Reducer Hooks 甚至只使用 State Hooks 可能就足够了。
第十三章:MobX 和 Hooks
- 哪些元素构成 MobX 的生命周期?
- 事件调用动作,从而修改状态。状态是可观察的,不应包含冗余或可推导的数据。计算值是通过纯函数从状态派生出来的。反应类似于计算值,但它们也可以产生副作用,比如在 React 中更新用户界面。
- MobX 提供哪些装饰器?
- MobX 为各种元素提供装饰器:
observer、observable、computed和action。
- 我们如何将组件连接到 MobX?
- 我们可以通过使用
Provider组件将我们的应用连接到 MobX 存储,然后通过inject高阶组件连接组件。如果我们希望组件在状态更改时自动重新渲染,还需要使用observer修饰函数来包装它。
- MobX 提供了哪些 Hooks?
- 我们可以使用
useObserverHook 来定义组件的部分,在状态更改时应该重新计算。
- 如何使用 Hooks 访问 MobX 存储?
- MobX 提供了一个上下文,可以用来创建访问 MobX 存储的自定义 Hooks。我们可以使用普通的上下文 Hook 来访问
mobx-react中的MobXProviderContext。
- 我们可以使用 MobX 存储本地状态吗?
- 是的,使用 MobX,我们可以创建任意数量的存储。MobX 甚至提供了一个
useLocalStoreHook 来创建本地存储。
- 我们应该如何将现有的 MobX 应用迁移到 Hooks?
-
我们可以逐步升级 MobX 应用的某些部分。我们可以使用自定义 Hook 来访问上下文的一部分,而不是
inject高阶组件。我们可以使用useObserverHook 来替代observer高阶组件。 -
我们应该首先使用 State Hook 来处理简单的本地状态,然后使用
useLocalStateHook 来处理复杂的本地状态,最后将全局状态保留在单独的 MobX 存储中。
- 使用 MobX 的优势是什么?
- 它提供了处理状态更改的简单方式,需要更少的样板代码,在应用代码结构方面提供了更多的灵活性,允许使用多个全局和本地存储,并通过将状态管理和操作交给 MobX 使
App组件变得更简单。
- 使用 MobX 的缺点是什么?
- 它允许状态更改发生在任何地方,而不仅仅是在单个存储中,这可能会使我们的应用更加不可预测。更多的灵活性也意味着可能以不好的方式构建项目并导致错误或缺陷。此外,如果我们想要获得所有功能,MobX 需要一个包装组件将应用连接到存储(我们可以直接导入和使用 MobX 存储,但这将破坏诸如服务器端渲染之类的功能)。
- 什么时候不应该使用 MobX?
- 如果状态更改简单且仅使用组件内的本地状态,则不应使用 MobX。在这种情况下,状态和 Reducer Hooks 可能足够了。