Skip to content

Latest commit

 

History

History
970 lines (667 loc) · 36.4 KB

File metadata and controls

970 lines (667 loc) · 36.4 KB

十三、MobX 和挂钩

在上一章中,我们学习了 Redux 以及如何将 Redux 与挂钩结合使用。我们还学习了如何将现有的 Redux 应用迁移到基于挂钩的解决方案。此外,我们还了解了使用 Reducer 挂钩和 Redux 挂钩的利弊,以及何时使用其中任何一种挂钩。

在本章中,我们将学习如何结合使用 MobX 和 hook。我们将从学习如何使用 MobX 处理状态开始,然后继续使用带有挂钩的 MobX。此外,我们将学习如何将现有的 MobX 应用迁移到 hook。最后,我们将讨论使用 MobX 的利弊。在本章结束时,您将完全了解如何使用挂钩编写 MobX 应用。

本章将介绍以下主题:

  • 了解什么是 MobX 及其工作原理
  • 使用 MobX 处理状态
  • 使用带有挂钩的 MobX
  • 迁移 MobX 应用
  • 了解 MobX 的权衡

技术要求

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

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

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

http://bit.ly/2Mm9yoC

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

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

什么是 MobX?

MobX 采用了与 Redux 不同的方法。它的目标不是应用限制使状态更改可预测,而是自动更新从应用状态派生的任何内容。在 MobX 中,我们可以直接修改 state 对象,而不是分派操作,MobX 将负责更新使用状态的任何内容。

MobX 生命周期的工作原理如下:

  1. 事件(如onClick)调用操作,这些操作是唯一可以修改状态的东西:
@action onClick = () => {
    this.props.todo.completed = true
}
  1. 状态是可观察的,不应包含冗余或可派生的数据。状态非常灵活,它可以包含类、数组、引用,甚至可以是图形:
@observable todos = [
    { title: 'Learn MobX', completed: false }
]
  1. 通过纯函数从状态导出计算值。这些将由 MobX 自动更新:
@computed get activeTodos () {
    return this.todos.filter(todo => !todo.completed)
}
  1. React 类似于计算值,但它们也会产生副作用而不是值,例如在 React 中更新用户界面:
const TodoList = observer(({ todos }) => (
    <div>
        {todos.map(todo => <TodoItem {...todo} />)}
    </div>
)

我们可以在下图中看到 MobX 生命周期的可视化:

Visualization of the MobX life cycle

MobX 和 React 配合得非常好。每当 MobX 检测到状态已更改时,它将导致相应组件的重新渲染。

与 Redux 不同,使用 MobX 不需要了解很多限制。我们只需要了解一些核心概念,如可观测值、计算值和 React。

现在我们已经了解了 MobX 的生命周期,让我们继续在实践中使用 MobX 处理状态。

使用 MobX 处理状态

了解 MobX 的最佳方法是在实践中使用它,并了解它是如何工作的。那么,让我们首先将我们的 ToDo 应用从第 11 章移植到 MobX,将从 React 类组件移植到 MobX。我们从复制Chapter11/chapter11_2/中的代码示例开始。

安装 MobX

第一步是通过npm安装 MobX 和 MobX React。执行以下命令:

> npm install --save mobx mobx-react

既然安装了 MobX 和 MobX React,我们就可以开始设置商店了。

设置 MobX 商店

安装 MobX 之后,是时候建立我们的 MobX 商店了。存储区将存储所有状态以及相关的计算值和操作。它通常由一个类定义。

现在让我们定义 MobX 存储:

  1. 创建一个新的src/store.js文件。
  2. 从 MobX 导入observableactioncomputed装饰器,以及decorate功能。这些将用于标记我们商店中的各种功能和值:
import { observable, action, computed, decorate } from 'mobx'
  1. 同时从我们的 API 代码中导入fetchAPITodosgenerateID函数:
import { fetchAPITodos, generateID } from './api'
  1. 现在,我们使用一个类来定义存储:
export default class TodoStore {
  1. 在这个存储中,我们存储一个todos数组和filter字符串值。这两个值是可观察的。稍后,我们将对其进行标记:
    todos = []
    filter = 'all'

With a special project setup, we could use an experimental JavaScript feature, known as decorators, to tag our values as observables by writing @observable todos = []. However, this syntax is not supported by create-react-app, since it is not part of the JavaScript standard yet.

  1. 接下来,我们定义一个计算值,以便从我们的存储中获得所有过滤的todos。该函数将类似于我们在src/App.js中使用的函数,但现在我们将使用this.filterthis.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
        }
    }
  1. 现在,我们定义我们的行动。我们从fetch动作开始。与之前一样,我们必须在稍后使用action装饰器标记动作函数。在 MobX 中,我们可以通过设置this.todos直接修改我们的状态。由于todos值是可观察的,因此 MobX 将自动跟踪其任何更改:
    fetch () {
        fetchAPITodos().then((fetchedTodos) => {
            this.todos = fetchedTodos
        })
    }
  1. 然后,我们定义我们的addTodo行为。在 MobX 中,我们不使用不可变值,因此不应该创建新数组。相反,我们总是修改现有的this.todos值:
    addTodo (title) {
        this.todos.push({ id: generateID(), title, completed: false })
    }

As you can see, MobX takes a more imperative approach, where values are directly modified, and MobX automatically keeps track of the changes. We do not need to use the rest/spread syntax to create new arrays; instead, we modify the existing state array directly.

  1. 接下来是toggleTodo动作。在这里,我们循环遍历所有的todos并使用匹配的id修改该项。请注意我们如何修改数组中的项,MobX 仍将跟踪更改。事实上,MobX 甚至会注意到数组中只有一个值发生了更改。与 React 结合使用,这意味着列表组件将不会重新呈现;只有更改的项的项组件将重新渲染。请注意,为了实现这一点,我们必须适当地划分我们的组件,例如单独列出列表和项目组件:
    toggleTodo (id) {
        for (let todo of this.todos) {
            if (todo.id === id) {
                todo.completed = !todo.completed
                break
            }
        }
    }

The for (let .. of ..) { construct will loop through all items of an array, or any other iterable value.

  1. 现在,我们定义removeTodo动作。首先,我们找到要删除的todo项的index
    removeTodo (id) {
        let index = 0
        for (let todo of this.todos) {
            if (todo.id === id) {
                break
            } else {
                index++
            }
        }
  1. 然后,我们使用splice从找到的元素的index开始移除一个元素。这意味着我们从数组中剪切出具有给定id的项:
        this.todos.splice(index, 1)
    }
  1. 我们定义的最后一个动作是filterTodos动作。在这里,我们只需将this.filter值设置为新过滤器:
    filterTodos (filterName) {
        this.filter = filterName
    }
}
  1. 最后,我们必须用前面提到的各种装饰师来装饰我们的商店。为此,我们在 store 类上调用decorate函数,并将对象映射值和方法传递给装饰器:
decorate(TodoStore, {
  1. 我们从todosfilter值开始,它们是可观察的:
    todos: observable,
    filter: observable,
  1. 然后我们装饰computed值*-*filteredTodos
    filteredTodos: computed,
  1. 最后但并非最不重要的是,我们装饰我们的行为:
    fetch: action,
    addTodo: action,
    toggleTodo: action,
    removeTodo: action,
    filterTodos: action
})

现在,我们的 MobX 商店已经装修好,可以使用了!

定义提供者组件

我们现在可以在App组件中初始化存储,并将其传递给所有其他组件。但是,最好使用 React 上下文。这样,我们就可以从应用中的任何位置访问商店。MobX React 提供了一个Provider组件,它在上下文中提供存储。

现在让我们开始使用Provider组件:

  1. 编辑src/index.js,从mobx-react导入Provider组件:
import { Provider } from 'mobx-react'
  1. 然后,从我们的store.js文件中导入TodoStore
import TodoStore from './store'
  1. 现在,我们创建TodoStore类的一个新实例:
const store = new TodoStore()
  1. 最后,我们必须将第一个参数调整为ReactDOM.render(),以便用Provider组件包装App组件:
ReactDOM.render(
    <Provider todoStore={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

Unlike Redux, with MobX, it is possible to provide multiple stores in our app. However, here, we only provide one store, and we call it todoStore.

现在,我们的存储已经初始化,可以在所有其他组件中使用。

连接部件

既然我们的 MobX 商店可以作为上下文使用,我们就可以开始将我们的组件连接到它了。为此,MobX React 提供了inject高阶组件,我们可以使用它将存储注入到我们的组件中。

在本节中,我们将把以下组件连接到我们的 MobX 商店:

  • App
  • TodoList
  • TodoItem
  • AddTodo
  • TodoFilter

连接应用组件

我们将首先连接App组件,当应用初始化时,我们将使用fetch操作从 API 获取所有todos

现在连接App组件:

  1. 编辑src/App.js,从mobx-react导入inject功能:
import { inject } from 'mobx-react'
  1. 然后,用inject包裹App组件。inject功能用于将存储(或多个存储)作为道具注入组件:
export default inject('todoStore')(function App ({ todoStore }) {

It is possible to specify multiple stores in the inject function, as follows: inject('todoStore', 'otherStore'). Then, two props will be injected: todoStore and otherStore.

  1. 现在我们有了可用的todoStore,我们可以使用它调用我们的效果挂钩中的fetch动作:
    useEffect(() => {
 todoStore.fetch()
    }, [ todoStore ])
  1. 我们现在可以移除filteredTodos备忘录挂钩、处理函数、StateContext.Provider组件以及我们传递给其他组件的所有道具:
    return (
        <div style={{ width: 400 }}>
            <Header />
            <AddTodo />
            <hr />
            <TodoList />
            <hr />
            <TodoFilter />
        </div>
    )
})

现在,我们的App组件将从 API 中获取todos,然后将它们存储在TodoStore中。

连接 TodoList 组件

todos存储在我们的商店后,我们可以从商店获取它们,然后我们可以在TodoList组件中列出所有 todo 项目。

现在连接TodoList组件:

  1. 编辑src/TodoList.js并导入injectobserver功能:
import { inject, observer } from 'mobx-react'
  1. 删除所有与上下文相关的导入和挂钩
  2. 和前面一样,我们使用inject函数包装组件。此外,我们现在用observer函数包装我们的组件。observer函数告诉 MobX,当存储更新时,该组件应重新呈现:
export default inject('todoStore')(observer(function TodoList ({ todoStore }) {
  1. 现在,我们可以使用我们商店中的filteredTodos计算值,列出应用过滤器的所有待办事项。为了确保 MobX 在item对象发生更改时仍然可以跟踪,我们在这里不使用扩展语法。如果使用排列语法,所有 todo 项都将重新呈现,即使只有一项发生了更改:
    return todoStore.filteredTodos.map(item =>
        <TodoItem key={item.id} item={item} />
    )
}))

现在,我们的应用将列出所有待办事项。但是,我们还不能切换或删除待办事项。

连接 TodoItem 组件

为了能够切换或删除待办事项,我们必须连接TodoItem组件。我们还将TodoItem组件定义为观察者,以便 MobX 知道当item对象发生变化时,它必须重新渲染该组件。

现在连接TodoItem组件:

  1. 编辑src/TodoItem.js,从mobx-react导入injectobserver功能:
import { inject, observer } from 'mobx-react'
  1. 然后,用injectobserver包裹TodoItem组件:
export default inject('todoStore')(observer(function TodoItem ({ item, todoStore }) {
  1. 我们现在可以在组件中使用item对象的解构。由于 MobX 被定义为观察者,因此它将能够跟踪item对象的更改,即使在分解结构之后:
    const { title, completed, id } = item
  1. 现在我们有了可用的todoStore,我们可以使用它来调整我们的处理函数,并调用相应的操作:
    function handleToggle () {
        todoStore.toggleTodo(id)
    }

    function handleRemove () {
        todoStore.removeTodo(id)
    }

现在,我们的TodoItem组件将调用todoStore中的toggleTodoremoveTodo操作,因此我们现在可以切换并删除 todo 项目!

连接 AddTodo 组件

为了能够添加新的待办事项,我们必须连接AddTodo组件。

现在连接AddTodo组件:

  1. 编辑src/AddTodo.js并从mobx-react导入inject功能:
import { inject } from 'mobx-react'
  1. 然后,用inject包裹AddTodo组件:
export default inject('todoStore')(function AddTodo ({ todoStore }) {
  1. 现在我们有了可用的todoStore,我们可以使用它来调整我们的处理函数,并调用addTodo操作:
    function handleAdd () {
        if (input) {
            todoStore.addTodo(input)
            setInput('')
        }
    }

现在,我们的AddTodo组件将从我们的todoStore调用addTodo操作,因此我们现在可以添加新的待办事项了!

连接 TodoFilter 组件

最后,我们必须连接TodoFilter组件,以便能够选择不同的过滤器。我们还想显示当前选择的过滤器,因此该组件需要是一个observer

现在连接TodoFilter组件:

  1. 编辑src/TodoFilter.js并导入injectobserver功能:
import { inject, observer } from 'mobx-react'
  1. 我们使用injectobserver函数包装组件:
const TodoFilterItem = inject('todoStore')(observer(function TodoFilterItemWrapped ({ name, todoStore }) {
  1. 现在,我们调整处理程序函数,从存储调用filterTodos操作:
    function handleFilter () {
        todoStore.filterTodos(name)
    }
  1. 最后,我们调整style对象以使用todoStore中的filter值,以检查当前是否选择了过滤器:
    const style = {
        color: 'blue',
        cursor: 'pointer',
        fontWeight: (todoStore.filter === name) ? 'bold': 'normal'
    }
  1. 此外,我们现在可以避免在FilterItem组件中传递道具。拆下以下用粗体标记的零件:
export default function TodoFilter (props) {
    return (
        <div>
            <TodoFilterItem {...props} name="all" />{' / '}
            <TodoFilterItem {...props} name="active" />{' / '}
            <TodoFilterItem {...props} name="completed" />
        </div>
    )
}

现在,我们可以选择新的过滤器,它将以粗体标记为 selected。todo 列表也将自动过滤,因为 MobX 检测到filter值的变化,这会导致filteredTodos计算值更新,并且TodoList观察者组件重新渲染。

示例代码

示例代码可在Chapter13/chapter13_1文件夹中找到。

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

使用带有挂钩的 MobX

在上一节中,我们学习了如何将 MobX 与 React 一起使用。正如我们所看到的,为了能够将我们的组件连接到 MobX 存储,我们需要使用inject函数包装它们,在某些情况下,还需要使用observer函数包装它们。除了使用这些高阶组件包装我们的组件之外,自从mobx-react版本的 v6 发布以来,我们还可以使用挂钩将我们的组件连接到 MobX 商店。我们现在将使用带有挂钩的 MobX!

定义存储挂钩

首先,我们必须定义一个挂钩才能访问我们自己的商店。正如我们之前所了解的,MobX 使用 React 上下文向各种组件提供并注入状态。我们可以从mobx-react获取MobXProviderContext并创建我们自己的自定义上下文挂钩,以便访问所有存储。然后,我们可以创建另一个挂钩,专门访问我们的TodoStore

那么,让我们开始定义一个商店挂钩:

  1. 创建一个新的src/hooks.js文件。
  2. react导入useContext挂钩,从mobx-react导入MobXProviderContext
import { useContext } from 'react'
import { MobXProviderContext } from 'mobx-react'
  1. 现在,我们定义并导出一个useStores挂钩,它为MobXProviderContext返回一个上下文挂钩:
export function useStores () {
    return useContext(MobXProviderContext)
}
  1. 最后,我们定义一个useTodoStore挂钩,它从前面的挂钩中获取todoStore,然后返回它:
export function useTodoStore () {
    const { todoStore } = useStores()
    return todoStore
}

现在,我们有一个通用的挂钩,可以从 MobX 访问所有商店,还有一个特定的挂钩可以访问TodoStore。如果需要,我们还可以在以后为其他商店定义更多挂钩。

将组件升级到挂钩

在创建一个挂钩来访问我们的存储之后,我们可以使用它来代替使用inject高阶组件函数包装我们的组件。在接下来的部分中,我们将看到如何使用挂钩升级各种组件。

使用应用组件的挂钩

我们将从升级App组件开始。可以逐步重构组件,以便使用挂钩。我们不需要一次重构每个组件。

现在让我们为App组件使用挂钩:

  1. 编辑src/App.js并删除以下import语句:
import { inject } from 'mobx-react'
  1. 然后,从我们的hooks.js文件中导入useTodoStore挂钩:
import { useTodoStore } from './hooks'
  1. 现在,移除包裹App组件的inject功能,移除所有道具。App函数定义现在应如下所示:
export default function App () {
  1. 最后,使用我们的 Todo Store 挂钩获取todoStore对象:
    const todoStore = useTodoStore()

正如你所看到的,我们的应用仍然像以前一样工作!然而,我们现在在App组件中使用了挂钩,这使得代码更加简洁明了。

对 TodoList 组件使用挂钩

接下来,我们将升级我们的TodoList组件。此外,我们还将使用useObserver挂钩,它将取代observer高阶组件。

现在让我们为TodoList组件使用挂钩:

  1. 编辑src/TodoList.js,删除以下导入语句:
import { inject, observer } from 'mobx-react'
  1. 然后,从mobx-react导入useObserver挂钩,从我们的hooks.js文件导入useTodoStore挂钩:
import { useObserver } from 'mobx-react'
import { useTodoStore } from './hooks'
  1. 现在,移除包裹TodoList组件的injectobserver功能,同时移除所有道具。TodoList函数定义现在应如下所示:
export default function TodoList () {
  1. 同样,我们使用 Todo Store 挂钩来获取todoStore对象:
    const todoStore = useTodoStore()
  1. 最后,我们用useObserver挂钩包装返回的元素。当挂钩中使用的状态更改时,将重新计算观察者挂钩中的所有内容:
    return useObserver(() =>
        todoStore.filteredTodos.map(item =>
            <TodoItem key={item.id} item={item} />
        )
    )
}

在我们的例子中,MobX 将检测通过useObserver挂钩定义的观察者依赖于todoStore.filteredTodos,而filteredTodos依赖于filtertodos值。因此,每当filter值或todos数组更改时,列表将重新呈现。

对 TodoItem 组件使用挂钩

接下来,我们将升级TodoItem组件,这将是一个与TodoList组件类似的过程。

现在让我们为TodoItem组件使用挂钩:

  1. 编辑src/TodoItem.js并删除以下import语句:
import { inject, observer } from 'mobx-react'
  1. 然后,从mobx-react导入useObserver挂钩,从我们的hooks.js文件导入useTodoStore挂钩:
import { useObserver } from 'mobx-react'

import { useTodoStore } from './hooks'
  1. 现在,移除包裹TodoItem组件的injectobserver函数,同时移除todoStore道具。TodoItem函数定义现在应如下所示:
export default function TodoItem ({ item }) {
  1. 接下来,我们必须删除分解(粗体代码),因为我们的整个组件不再被定义为可观察的,所以 MobX 将无法跟踪对item对象的更改:
 const { title, completed, id } = item
  1. 然后,使用 Todo Store 挂钩获取todoStore对象:
    const todoStore = useTodoStore()
  1. 现在,我们必须调整处理函数,以便它们直接使用item.id而不是id。请注意,我们假设id没有变化,因此,它没有被包装在观察者挂钩中:
    function handleToggle () {
        todoStore.toggleTodo(item.id)
    }

    function handleRemove () {
        todoStore.removeTodo(item.id)
    }
  1. 最后,我们用一个 Observer 挂钩包装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挂钩中,如下所示:

    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语句包装在单独的观察者挂钩中,因为这样处理程序函数只能在第一个观察者挂钩的闭包中定义。这意味着我们将无法从第二个观察者挂钩中访问处理程序函数。

接下来,我们将继续使用AddTodo组件的挂钩来升级我们的组件。

使用 AddTodo 组件的挂钩

我们在AddTodo组件的App组件中重复相同的升级过程,如下所示:

  1. 编辑src/AddTodo.js并删除以下import语句:
import { inject } from 'mobx-react'
  1. 然后,从我们的hooks.js文件中导入useTodoStore挂钩:
import { useTodoStore } from './hooks'
  1. 现在,移除包裹AddTodo组件的inject功能,同时移除所有道具。AddTodo函数定义现在应如下所示:
export default function AddTodo () {
  1. 最后,使用 Todo Store 挂钩获取todoStore对象:
    const todoStore = useTodoStore()

现在,我们的AddTodo组件已连接到 MobX 商店,我们可以继续升级TodoFilter组件。

对 TodoFilter 组件使用挂钩

对于TodoFilter组件,我们将使用与TodoList组件类似的过程。我们将使用我们的useTodoStore挂钩和useObserver挂钩。

现在让我们为TodoFilter组件使用挂钩:

  1. 编辑src/TodoFilter.js并删除以下import语句:
import { inject, observer } from 'mobx-react'
  1. 然后,从mobx-react导入useObserver挂钩,从我们的hooks.js文件导入useTodoStore挂钩:
import { useObserver } from 'mobx-react'
import { useTodoStore } from './hooks'
  1. 现在,移除包裹TodoFilterItem组件的injectobserver函数,同时移除todoStore道具。TodoFilterItem函数定义现在应如下所示:
function TodoFilterItem ({ name }) {
  1. 同样,我们使用 Todo Store 挂钩来获取todoStore对象:
    const todoStore = useTodoStore()
  1. 最后,我们用useObserver挂钩包裹style对象。记住,当挂钩中使用的状态发生变化时,观察者挂钩中的所有内容都将重新计算:
    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(如果它没有自动打开)。

使用本地商店挂钩

除了提供全局存储以存储应用范围的状态外,MobX 还提供本地存储以存储本地状态。要创建本地商店,我们可以使用useLocalStore挂钩。

我们现在将在AddTodo组件中实现本地存储挂钩:

  1. 编辑src/AddTodo.js并从mobx-react导入useLocalStore挂钩和useObserver挂钩:
import { useLocalStore, useObserver } from 'mobx-react'
  1. 然后,移除以下状态挂钩:
    const [ input, setInput ] = useState('')

将其替换为本地存储挂钩:

    const inputStore = useLocalStore(() => ({

在这个本地存储中,我们可以定义状态值、计算值和操作。useLocalStore挂钩将自动将值修饰为可观察值,将 getter 函数(前缀为get)修饰为计算值,将正常函数修饰为动作。

  1. 我们从input字段的value状态开始:
        value: '',
  1. 然后,我们定义一个计算值,它将告诉我们添加按钮是否应该是disabled
        get disabled () {
            return !this.value
        },
  1. 接下来,我们定义动作。第一个操作从输入事件更新value
        updateFromInput (e) {
            this.value = e.target.value
        },
  1. 然后,我们定义另一个操作来从一个简单字符串更新value
        update (val) {
            this.value = val
        }
    }))
  1. 现在,我们可以调整输入处理函数,并调用updateFromInput操作:
    function handleInput (e) {
        inputStore.updateFromInput(e)
    }
  1. 我们还需要调整handleAdd功能:
    function handleAdd () {
        if (inputStore.value) {
            todoStore.addTodo(inputStore.value)
            inputStore.update('') }}
  1. 最后,我们用一个useObserver挂钩包装元素,以确保input字段值在变化时得到更新,并调整disabledvalue道具:
    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 应用

在上一节中,我们学习了如何用 hook 替换现有 MobX 应用中的 MobX 高阶组件,例如injectobserver。现在,我们将学习如何将本地状态迁移到现有 MobX 应用中的挂钩。

可以通过以下三个步骤将现有 MobX 应用迁移到基于挂钩的解决方案:

  • 对简单的本地状态使用状态挂钩
  • 使用useLocalState挂钩处理复杂的局部状态
  • 在单独的 MobX 存储中保持全局状态

在本书的前几章中,我们已经学习了如何使用状态挂钩。状态挂钩对于简单状态(例如复选框的当前状态)是有意义的。

在本章中,我们已经学习了如何使用useLocalState挂钩。我们可以对复杂的局部状态使用局部状态挂钩,例如多个字段相互作用的复杂表单。然后,我们可以将多个状态和效果挂钩替换为单个局部状态挂钩以及计算值和操作。

最后,全局状态应该存储在单独的 MobX 存储中,例如我们在本章中定义的TodoStore。在 MobX 中,可以使用Provider组件创建多个存储并传递给组件。然后,我们可以为每个商店创建一个单独的自定义挂钩。

MobX 的权衡

最后,让我们总结一下在 web 应用中使用 MobX 的优缺点。首先,让我们从积极的方面开始:

  • 它提供了一种处理状态更改的简单方法
  • 需要更少的样板代码
  • 它为应用代码的结构提供了灵活性
  • 可以使用多个全局和本地存储
  • 它使App组件更简单(它将状态管理和操作卸载到 MobX)

MobX 非常适合处理复杂状态更改的小型和大型项目,以及跨许多组件使用的状态

但是,使用 MobX 也有缺点:

  • 状态变化可能发生在任何地方,而不仅仅是在单个商店中
  • 它的灵活性意味着可能以一种糟糕的方式构建项目,这可能会导致错误或 bug
  • 如果我们想获得所有功能,MobX 需要一个包装器组件(Provider),以便将应用连接到应用商店(我们可以直接导入并使用 MobX 应用商店,但它会破坏服务器端渲染等功能)

如果状态更改很简单,并且只需要组件内的本地状态,则不应使用 MobX。在这种情况下,一个状态或简化器挂钩就足够了。有了 Reducer 和 State 挂钩,就不需要包装器组件来将我们的应用连接到应用商店。

灵活性是一件好事,但它也会导致我们的项目结构不好。然而,MobX 提供了一个名为mobx-state-tree的项目,它允许我们使我们的 MobX 应用更加结构化,并实施某种架构。更多信息可在以下 GitHub 存储库的项目页面上找到:https://github.com/mobxjs/mobx-state-tree

总结

在本章中,我们首先了解了什么是 MobX,它由哪些元素组成,以及它们是如何协同工作的。然后,我们学习了如何在实践中使用 MobX 进行状态管理。我们还学习了如何通过使用injectobserver高阶组件连接 MobX 存储以 React 组件。接下来,我们用挂钩替换了高阶组件,这使我们的代码更加干净简洁。我们还学习了如何在 MobX 中使用本地存储挂钩来处理复杂的本地状态。最后,我们学习了如何将现有的 MobX 应用迁移到 Hooks,并回顾了使用 MobX 的利弊。

这一章标志着这本书的结束。在这本书中,我们从使用挂钩的动机开始。我们了解到,React 应用中存在一些常见问题,如果没有挂钩,这些问题很难解决。然后,我们使用挂钩创建了第一个组件,并将其与基于类组件的解决方案进行了比较。接下来,我们深入了解了各种挂钩,从最普遍的状态挂钩开始。我们还学习了如何使用挂钩解决常见问题,例如条件挂钩和循环中的挂钩。

在深入了解状态挂钩之后,我们使用挂钩开发了一个小型博客应用。然后我们学习了 Reducer 挂钩、Effect 挂钩和 Context 挂钩,以便能够在我们的应用中实现更多功能。接下来,我们学习了如何使用挂钩高效地请求资源。此外,我们还学习了如何使用React.memo防止不必要的重新渲染,以及如何使用 React Suspense 实现延迟加载。然后,我们在我们的博客应用中实现了路由,并学习了挂钩如何使动态路由变得更容易。

我们还了解了社区提供的各种挂钩,这些挂钩使处理输入字段、各种数据结构、响应性设计和撤销/重做功能变得更加容易。此外,我们还了解了挂钩的规则,如何创建自己的自定义挂钩,以及挂钩之间的交互是如何工作的。最后,我们学习了如何有效地从现有的基于类组件的应用迁移到基于挂钩的解决方案。最后,我们学习了如何在 Redux 和 MobX 中使用 hook,以及如何将现有的 Redux 和 MobX 应用迁移到 hook 中。

现在我们已经深入了解了挂钩,我们已经准备好在应用中使用它们了!我们还学习了如何将现有项目迁移到挂钩,现在就可以开始了。我希望您喜欢学习 React 挂钩,并期待在应用中实现挂钩!我确信使用挂钩将使您的编码更加愉快,就像它们为我所做的那样。

问题

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

  1. 哪些元素构成 MobX 生命周期?
  2. MobX 提供哪些装饰器?
  3. 我们如何将组件连接到 MobX?
  4. MobX 提供哪些挂钩?
  5. 我们如何使用 hook 访问 MobX 商店?
  6. 我们可以使用 MobX 存储本地状态吗?
  7. 我们应该如何将现有的 MobX 应用迁移到 hook?
  8. 使用 MobX 的优点是什么?
  9. 使用 MobX 的缺点是什么?
  10. 什么时候不应该使用 MobX?

进一步阅读

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