ReactNative-状态管理简化指南-二-

69 阅读1小时+

ReactNative 状态管理简化指南(二)

原文:zh.annas-archive.org/md5/fd5930eef40de8fb3cdfa93d7033a35c

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:在 React Native 应用中使用 MobX 作为状态管理器

在上一章中,我们有机会尝试在 FavoritedImages 上下文中使用最受欢迎的状态管理解决方案 Redux。您可以随时返回 GitHub 仓库的文件夹 第五章 检查代码中具体发生了哪些变化:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-5

如果您想看到整个应用完全迁移到 Redux,请访问另一个文件夹:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-5-complete

到目前为止,我们面临了一个陡峭的学习曲线。我们讨论了使用状态管理库的 FavoritedImages。在本章中,我们将讨论使用 MobX 状态、模型和动作的 FavoritedImages 上下文。

本章将包括以下内容:

  • 复习 MobX 概念

  • 在 Funbook 应用中配置 MobX

  • 使用 FavoritedImages

到本章结束时,您应该能够熟练使用 MobX。您不仅将了解 MobX 模型、快照和存储是什么,而且您还将知道您是否更喜欢它们而不是 Redux!这正是本书的真正目的:了解不同的解决方案,以便您可以为未来的项目选择您更喜欢的方案。

技术要求

为了跟随本章的内容,您需要了解一些 JavaScriptReactJS 的知识。如果您至少跟完了本书的 第一章第四章,您应该能够无任何问题地继续前进。

随意使用您选择的 IDE,因为 React Native 不需要任何特定功能。目前,前端开发者中最受欢迎的 IDE 是微软的 VSCode、Atom、Sublime Text 和 WebStorm。

本章提供的代码片段旨在说明我们应该如何使用代码。它们并不提供完整的画面。为了更容易地编码,请打开您 IDE 中的 GitHub 仓库并查看其中的文件。您可以从名为 example-app-fullchapter-6 的文件夹中的文件开始。如果您从 example-app-full 开始,您将负责实现本章中描述的解决方案。如果您选择查看 chapter-6,您将看到我实现的整个解决方案。

如果您遇到困难或迷失方向,可以检查 GitHub 仓库中的代码:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-6

复习 MobX 概念

正如你可能已经注意到的,亲爱的读者,我喜欢在每个大节开始时,简要介绍一下我们将要检查的软件的历史。碰巧 MobX 在 React 社区中有着非常平静的存在。它的构思或开发过程中并没有真正的戏剧性。它在 2015 年由 Mendix 公司的博客宣布,MobX 的创造者Michel Weststrate曾在这里工作。博客文章详细介绍了创建这个库的原因,即 2015 年的纯 ReactJS 应用在管理复杂状态方面并不很好。从那时起,MobX 已经在 GitHub 上作为一个开源库被开发。2016 年,它加入了MobX-State-TreeMST),这是 MobX 的状态容器系统。MST 对于 MobX 来说,就像 Redux Toolkit 对于 Redux 一样。它是一个为更好的开发者体验DX)而制作的额外工具,但它不是必需的。我个人喜欢让我的生活更简单,所以在这本书中,我们将使用 MST。

我与Jamon Holmgren交换了几条信息,他是 Infinite Red 的 CTO,Infinite Red 是一家在 React Native 领域享有盛誉的软件公司,同时也是MST的维护者。他说他大约 5 年前得知MobX,当时他的队友们在寻找 Redux 的替代品。在完成一个试验项目后,他们非常喜欢它,并且一直在使用它。它甚至已经集成到 Infinite Red 的React Native模板Ignite中。Jamon 说,“MST的主要优势是,你可以在不触及每个更改时需要触摸四个或五个不同文件的情况下,获得 Redux 的中心存储感觉。你还可以获得细粒度的重新渲染,而不需要编写单个选择器,并且感觉非常自然。Infinite Red 的开发者在拥有数百个屏幕和数百万日活跃用户的 App 上使用 MST 时几乎没有问题,因此它是一个经过验证的、与 React 和 React Native 配合得非常好的状态管理系统。”在开发者需要与更不结构化的数据一起工作,并且需要更多控制的情况下,MobX可能比MST是更好的解决方案。

MobX仍然带来了 MST 所具有的可观察性(细粒度、有针对性的重新渲染)和自然的更新,但重量更轻,”Jamon 补充道。

MobX大约 7 年前被创建,但多年来一直保持着相关性。Jamon 说,他希望改进库的TypeScriptTS)类型,但总体来说,他认为由于作者 Michel Weststrate 出色的工程,这个库表现得非常好。

MobX 目前是 React 应用程序中最受欢迎的状态管理库之一。文档中提到,它是最受欢迎的 Redux 替代方案之一。如果你仔细阅读文档,可能会发现作者暗示 MobX 比起 Redux 更好。当我问及这种竞争关系时,Jamon 说:“与其他优秀的社区争论总是很有趣。现实是,MobX 社区非常尊重 Redux 社区。他们的社区推动我们变得更好,并不断进步。他们做出了不同的权衡决策,可能不是你的特定风格,所以有选择权是件好事。”

当然,MobX 维护者有完全的权利认为他们正在工作的解决方案更好。现在,让我们看看你,亲爱的读者,是怎么想的!

关于 MobX 的概念和高级理念,文档中有一句非常重要的话被加粗了:

应该从应用状态中推导出任何可以推导的东西。自动地。

  • MobX 口号

这是一个新概念!任何可以推导出的东西都应该自动推导。我们之前是否自动从我们的应用状态中推导出任何东西?实际上并没有。最初,我们创建了 useStateuseEffect 钩子,与 React 上下文结合使用。每当用户与我们的应用交互时,我们必须手动更新所有必要的状态部分。在 Redux 中,我们编写了动作,并将状态更新的信息传递给 reducer。我们可以说状态更新是自动发生的;在传递动作后,我们不需要执行任何额外的任务。然而,我们确实创建了动作并手动调用它。我们还知道 Redux 并不特别提倡从应用状态中推导值。Redux 文档更多地集中在不可变性、状态是单一真相来源以及使用纯函数。

MobX 文档指出,这个库基于透明的函数式编程——这一概念在由 Packt Publishing 出版的《MobX 快速入门指南》一书中得到了进一步解释。MobX 的哲学是以下这些:

  1. 简单直接 – 编写简约的代码,反应系统将自动检测所有更改,无需添加特殊工具或样板代码。

  2. 轻松优化 – 数据更改在运行时跟踪,这意味着计算只在需要时运行,我们避免了不必要的组件重新渲染。

  3. 无偏见MobX 可以与任何 UI 框架一起使用,这使得你的代码解耦、可移植,并且易于测试。

MobX-land 中还有一个有趣的概念,那就是快照。如果你曾经为 JavaScript 应用程序编写过测试,你可能听说过“快照”这个术语。MobX 快照与测试快照类似。它们在特定时间点保存状态树的状态。在调试期间查看 MobX 快照或在从服务器获取数据后进行高效的状态更新时,这可能会非常有用。如果你想了解更多关于快照和调试 MobX 状态的信息,我邀请你查看由 MobX 的创造者 Michel Westrate 创建的 Egghead.io 课程;你可以在 进一步阅读 部分找到链接。至于从服务器获取数据,我们将在本章的最后部分探讨这个问题。

现在,我们对 MobX 的主要概念有了非常理论性的了解。我们知道它与 Redux 不同,但亲爱的读者,你可能想看到一些代码!让我们继续在 Funbook 应用中配置 MobX

在 Funbook 应用中配置 MobX

MobX 作者所承诺的,这个库的样板代码是最小的。我们需要添加三个依赖项和一些文件,才能使一切正常工作。让我们首先通过在终端运行以下命令来添加必要的依赖项:

npm install mobx mobx-state-tree –save

此命令将安装 MobXMobX-State-Tree。MobX 对我们想要与之一起使用的 UI 库没有意见。这意味着当我们决定使用特定的 UI 库时,我们必须找到一种方法让它与 MobX 合作。碰巧我们选择了 React Native 作为我们的 UI 库,因此我们需要添加一个额外的依赖项,以便 MobX 与 React 平滑合作。让我们运行以下命令:

npm install mobx-react-lite –save

现在我们有了依赖项,让我们运行以下命令:

expo start

经常检查我们的应用是否仍然正常运行是个好主意。像安装依赖项这样无害的事情有时可能会破坏应用,我们希望尽快知道任何问题。

假设一切按预期工作,我们可以继续在 Funbook 应用中实现 MobX 而不是 React 的上下文。

一个小提醒,亲爱的读者,关于代码:与本章相关的代码可以在这本书的仓库的 chapter-6 文件夹中找到:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-6。如果你更喜欢自己跟随,请复制 example-app-full 文件夹并从这里开始工作。

在 FavoritedImages 中使用 MobX

在本书的开头,我亲爱的读者,我做出了一个选择,那就是用 JavaScript 编写所有示例。在用 MobX 编写示例时,我对此决定感到后悔。MobX 文档使用 TS,这是 JavaScript 的超集,它带来了许多优势。我鼓励我的亲爱的读者去了解 TS。我不会在这个话题上花费更多时间,因为网上和书籍中都有数百种非常有价值的 TS 资源,但我想要让你知道,以防你阅读 MobX 文档,示例看起来与这本书中的代码略有不同。

现在我们已经把这些都弄清楚了,让我们开始编码!我们将创建一个名为 models 的新文件夹,我们将在这个文件夹中存储我们应用程序的数据模型。术语“数据模型”可能听起来非常严肃,但请放心。MobX 数据模型不过是带有超级能力的 JavaScript 对象——我的意思是,它们看起来像简单的 JavaScript 对象,但它们能够做更多的事情!

当我们有几个模型准备就绪时,我们将为我们的全局 store.js 创建一个额外的文件,并将获取和管理喜欢图像的所有逻辑放在这个文件中。

让我们先创建最简单的模型:用户的模型。我们不会实现实际的用户状态变更,但我们会快速看一下在现实世界的实现中 MobX 模型是什么样的:

// ./models/User.js
import { types } from "mobx-state-tree"
export const User = types.model({
    name: types.string,
    loggedIn: types.boolean,
})

我们只需要导入一个项目:来自 mobx-state-treetypes。这些类型是 MobX 中非常强大的工具。你可以声明非常简单的类型,就像这里的一样——一个字符串和一个布尔值——但你也可以声明这些值是可选的,如下所示:

name: types.optional(types.string, "")

你也可以在先前的示例中 types.string 定义后面的 "" 符号表示),或者一个给定的值可能是未定义的,如下所示:

name: types.maybe(types.string)

还有许多其他类型,但我们不会涵盖所有这些。然而,MST 文档有一个关于类型的非常详尽的章节,你可以在 进一步 阅读 部分找到这个链接。

你可能已经注意到 types.model 也位于声明的非常开始的位置。这就是告诉 MobX 我们正在描述我们数据形状的标志。

我们的 Users 模型非常简单。我们用它来初步了解 LikedImages 模型。

我们再次开始,通过从 mobx-state-tree 导入 types 并声明单个 LikedImage 项的形状:

// ./models/LikedImages
import { types } from "mobx-state-tree"
const LikedImageItem = types
    .model({
        itemId: types.number,
        authorId: types.number,
        timeStamp: types.string,
        url: types.string,
        likes: types.string,
        conversations: types.string,
    })

我们在 LikedImageItem 模型中添加了一些属性。我们将在未来使用这些属性在 Favorited 表面上显示必要的数据。恰好这些属性存在于从服务器获取的图像项中。

现在已经描述了单个图像模型,我们可以继续设置相同图像的数组以及与此数组相关的操作:

export const LikedImages = types
    .model({
          imageList: types.optional             (types.array(LikedImageItem), []),
    })
    .actions(self => ({
        addLikedImage(newImage) {
            // will add images here
        },
        removeLikedImage(imageToRemove) {
            // will remove images here
        },
    }))

从顶部开始,您会注意到我们正在声明一个名为 imageList 的对象,它将存储一个 LikedImageItems 的数组,并将使用空数组的默认值进行实例化。

LikedImageItem 模型并没有做什么有趣的事情,所以我们继续到 LikedImages 数组。我们必须添加一个 types.model,告诉我们的状态管理器这个状态片段将是一个 LikedImageItems 的数组——然后我们添加两个需要创建的函数的占位符:添加和删除喜欢的图片。

我们现在可以继续在我们的应用程序中设置 MobX。首先,我们将设置一个存储——类似于 Redux 管理的应用程序,这将成为应用程序的真相来源。然后我们将从服务器获取数据并将其传递给应用程序。一旦我们准备好了所有这些,我们将查看 MobX 动作——我们的模型需要响应的事件。最后,但同样重要的是,我们将了解从状态中推导数据。

创建存储

在添加和删除图片之前,我们还需要采取一个步骤。亲爱的读者,您认为呢?是的,我们需要连接到存储!

让我们转到我们的 store.js 文件,并告诉它使用 UserLikedImages 模型。我们将首先导入所有必要的文件并创建一个空的存储:

import { types, flow, applySnapshot } from "mobx-state-tree"
import { LikedImages } from "./src/models/LikedImages";
import { User } from './src/models/User';
const RootStore = types
    .model({
        users: User,
        likedImages: LikedImages
    })
export const store = RootStore.create({
    users: {},
    likedImages: {}
})

如您所记,亲爱的读者,MobXMST 在 UI 方面都是无偏见的。这意味着我们需要寻找如何将 MST 与我们的 React Native 应用程序最佳集成的详细说明。碰巧的是,文档建议使用 React 的上下文在组件之间共享树。我们的例子目前还很小,我们将专注于一个树(收藏的图片);然而,为我们的应用程序扩展正确设置是很重要的。还有:我们不是从之前的章节中很好地理解了上下文,对吧?所以,这将是一件轻而易举的事情:

const RootStoreContext = React.createContext(null);
export const Provider = RootStoreContext.Provider;
export function useMst() {
    const store = useContext(RootStoreContext);
  if (store === null) {
      throw new Error("Store cannot be null,        please add a context provider");
  }
  return store;
}

在前面的代码中,我们创建了一个非常简单的上下文,它将成为 useMst 钩子的载体(也就是说,“使用 null,当我们向应用程序添加 <Provider> 时,我们将传递真实的存储):

// App.js
//…
Import { Provider, store } from "./store.js"
//…
export default function App() {
//…
  return (
    <SafeAreaProvider>
      //…
          <Provider value={store}>

记得将您的应用程序包裹在为 MobX 状态创建的 Provider 中。这就是前面代码片段中显示的内容。

现在我们已经声明了存储和我们的模型,将应用程序包裹在 Provider 中,并将存储传递给这个 Provider,我们需要从 ListOfFavorited.js 中拉取数据,并用之前使用的纯 React 上下文替换 MobX 数据:

import { useMst } from '../../store';
export const ListOfFavorites = ({ navigation }) => {
  const { likedImages } = useMst();
  //…
  return (
    //…
    >
      <FlatList
        data={likedImages.imageList}
        //…

这进行得相当顺利,不是吗?我们的 ListOfFavoritedImages 组件已经准备好了!是的?让我们检查一下应用程序:

图 6.1 – 没有图片的收藏表面

图 6.1 – 没有图片的收藏表面

Favorited 表面上,我们只看到了一个空白屏幕。发生了什么?我们忘记获取图片了!让我们看看如何在下一节中做到这一点。

获取数据

我们在服务器上存储了图像列表。MobX-State-Tree提出了两种获取异步数据的方法,但两者都是操作。让我们在存储中创建一个操作:

// ./store.js
const RootStore = types
    .model({
        users: User,
        likedImages: LikedImages
    })
    .actions(self => ({
        async fetchImages() {
               const response = await fetch(requestBase + "/                 john_doe/likedImages.json");
             const data = await response.json();
             return data;
        }
    }))

我们需要一个异步函数来执行获取操作——我们将其命名为fetchImages。这个函数使用了 JavaScript 的fetch函数,并从服务器返回数据。现在我们有了数据,我们需要将其传递给LikedImages模型。让我们添加一个函数来完成这项工作:

// ./store.js
const RootStore = types
    //…
    .actions(self => ({
        setLikedImages(newImages) {
            store.likedImages.imageList.replace(newImages)
        },
        async fetchImages() {
               const response = await fetch(requestBase + "/                 john_doe/likedImages.json");
             const data = await response.json();
             store.setLikedImages(data);
        }
    }))

新增的setLikedImages函数负责用传递给它的任何内容替换整个图像数组。我们还调整了fetchImages函数,以便将获取的结果传递给setLikedImages

现在我们已经告诉我们的应用从哪里获取数据以及将其放在哪里,我们只需要添加“何时”。我们可以在应用渲染时直接从应用中调用store.fetchImages()函数。然而,有一个更优雅的解决方案:使用afterCreate提供的生命周期钩子,正如你可能预期的,它是在创建给定存储之后调用的。让我们将这个钩子添加到我们存储中的操作列表中:

// ./store.js
const RootStore = types
    //…
    .actions(self => ({
        afterCreate() {
            self.fetchImages();
        },
        //…
    }))

哇!我们的应用将知道从哪里获取数据(服务器上的数据),一旦获取到数据后将其放在哪里(在LikedImages数组中),以及何时进行操作(当存储创建时)。如果你现在检查应用,你应该能看到正确渲染的图像列表。

我们编写的代码运行良好,但我们可以进一步改进它。MobXMST为我们提供了编写异步逻辑的优化解决方案。他们的解决方案被称为生成器函数。一开始这可能听起来有些吓人,但别担心。我们只需要从 MST 导入几个实用工具,并稍微改变一下函数的语法:

// ./store.js
 import { types, flow, applySnapshot } from "mobx-state-tree"
//…
    .actions(self => ({
        afterCreate() {
            self.fetchImages();
        },
        fetchImages: flow(function* fetchImages() {
              const response = yield fetch(requestBase + "/                john_doe/likedImages.json");
              applySnapshot(self.likedImages.imageList,                yield response.json());
        })

这个版本的fetchImages函数使用了生成器。对于flow,使用*function关键字一起。然后,我们将async/await替换为yield,这会暂停函数并返回一个Promise

正如你可能已经注意到的,我们在这一版本的代码中移除了setLikedImages操作。它不再需要,因为我们正在使用另一个applySnapshot。我之前简要地提到了applySnapshot实用工具中的快照,我们确保更新是优化的,因为只有必要的数据被更新。

这个代码版本的输出结果与上一个版本相同。然而,它使用了更少的代码行,并且采用了MobX作者推荐的最佳实践。按照推荐的方式编写代码是个好主意——这有助于我们避免错误和性能问题。我们关于MobX的了解肯定不如其作者和维护者多,所以让我们跟随他们的脚步。

好的——我们在这里取得了很大的进展。我们已经有了数据模型,并将它们连接到了存储中。我们通过Provider将存储传递到我们的应用中,并获取了初始数据。现在唯一剩下的事情就是添加操作,让这个应用活跃起来!

添加操作

让我们回到LikedImages模型,并为addImages操作添加一些真正的代码:

.actions(self => ({
        addLikedImage(newImage) {
            self.imageList.unshift(newImage)
        },

actions函数本身持有整个喜欢图片数组的引用——这就是self关键字。在this的第一个迭代中,this对于许多开发者来说可能很令人困惑,这就是为什么使用self。此外,MobX意识到如果你在一个模型上执行操作,你可能需要访问该模型,所以它为我们提供了我们需要的东西!

现在我们有了LikedImages数组的引用,我们想要向该数组添加一个新项目。我们可以使用.push(),但我选择使用.unshift(),这将把新项目推送到数组的顶部,并有效地在Favorites表面的图片列表顶部显示它。

我们希望调用此操作的地点是ImageDetailsModal,因为我们可以在其中“喜欢”图片。这个模态有一个心形按钮。当它被点击时,我们希望将图片添加到我们用户的喜欢图片数组中:

// ./surfaces/ImageDetailsModal.js
export const ImageDetailsModal = ({ navigation, route }) => {
  const { likedImages } = useMst();
//…
<Pressable
        onPress={() => {
            likedImages.addLikedImage(route.params.imageItem)
        }}
      >

美妙!现在,当我们从主动态中点击这个可按压的心形图标时,我们应该看到图片被添加到收藏表面,对吧?不幸的是,目前还没有。给ListOfFavorited组件添加了observer包装器。这个observer包装器会在检测到数据模型变化时重新渲染我们的组件:

// ./components/ListOfFavorited
import { useMst } from '../../store';
import { observer } from "mobx-react-lite"
export const ListOfFavorites = observer(({ navigation }) => {
  const { likedImages } = useMst();

现在我们几乎完成了!只剩一个小问题。当你喜欢一张图片然后返回收藏表面时,你可能直到开始滚动才会看到新图片。这不是我们想要的功能。我们希望新喜欢的图片能立即显示。这里的问题是FlatList组件,它接受简单的数组,但我们正在尝试从我们的MobX模型传递一种特殊的数组:一个可观察的数组。

让 FlatList 与 MobX 和谐共存

为了让我们的FlatList正确渲染更新后的数据,我们需要使用 MobX 提供的values实用工具。

这是ListOfFavorited组件中FlatList的代码:

Import { values } from "mobx"
<FlatList
        data={values(likedImages.imageList)}

Values是 MST 库提供的一个集合实用工具,它返回集合中的所有值作为一个数组,这正是FlatList所期望的。你可以在MobX的文档中了解更多关于集合实用工具的信息,并在进一步阅读部分找到链接。

现在,一切应该都按预期工作。请确保经常检查你的手机或手机模拟器。越早发现错误和问题,调试起来就越容易。

从状态中推导数据

我提到MobX的作者表示,任何可以从状态中推导出的东西都应该推导。我们现在将有机会推导一些数据。

我们想知道哪些图片被喜欢了,哪些没有被喜欢,这样我们才能成功地将它们添加到喜欢图片列表中或避免重复。从状态中推导数据是通过views在数据模型中完成的。我决定将以下视图添加到存储中,因为我们在一个受限的环境中工作,我想保持事情简单。这是RootStore模型:

const RootStore = types
 //…
    .views(self => ({
        getIsImageLiked(itemId) {
            return values(self.likedImages?.imageList).filter(
                      (favoritedImg) => favoritedImg.itemId ===                        itemId
                  ).length > 0;
        }
    }))

就像actions一样,你在这里会注意到self关键字。它持有对当前数据模型的引用,以便于访问。

我通过传递一个图片 ID 创建了一个getIsImageLiked函数。然后我们过滤整个喜欢的图片数组来检查该图片 ID 是否存在。

当然,这不是检查社交媒体应用中用户喜欢的图片的最有效方法,这些图片可能成百上千——但我们确实想看看这些视图的内容,这是一个很好的机会。

让我们回到ImageDetailsModal,我们想要检查一个给定的图片是否被喜欢,然后显示相应的图标(未喜欢的图片为空心形,喜欢的图片为实心形),并传递适当的函数(要么添加到喜欢的图片数组中,要么从其中移除)。

如果你从example-app-full文件夹复制了你的代码,你会在该组件中找到useEffect,它负责检查这个确切的事情。让我们尝试简单地用来自MobX存储的新值替换旧的 React 上下文值。代码工作了吗?请继续检查,我就在这里等你。

有什么不对劲的地方吗?代码没有按预期工作。说实话,它根本不起作用。如果你试图一步一步地弄清楚在useEffect变化之间发生了什么,以及应该发生什么,你可能发现这并不简单。副作用优先级可能非常复杂,在大型的应用程序中更是如此——这就是为什么我们使用MobX的专用工具:视图。

回到我们的代码,我们可以完全移除useEffect。我们在views中处理过滤,这些views被添加到存储中。让我们使用来自上下文钩子的import并使用MobX提供的值:

  export const ImageDetailsModal = observer(({ navigation,    route }) => {
  const { likedImages, getIsImageLiked } = useMst();
    const isCurrentImageLiked = getIsImageLiked       (route.params.imageItem.itemId)

不要忘记为我们的组件添加observer包装器以观察数据变化!

现在心形图标按预期工作——当图片在Favorited表面被喜欢时,它看起来是填充的,当未喜欢的图片被新喜欢时,它也会被填充。

如果你只想看到完整的应用程序,我们已经在chapter-6-complete文件夹中创建了数据模型,设置了存储、操作和视图。

摘要

我们刚刚讨论了observer包装器的主要思想和实现,这些包装器用于需要知道状态变化的组件,然后我们有一个非常棒的MobX管理的应用程序。

了解如何在React Native应用程序中管理状态是非常好的。知道几种不同的方法来做这件事就更好了——如果你喜欢不同的选项,你将很高兴地知道,我们将在下一章讨论XState

进一步阅读

第七章:使用 XState 解开 React Native 应用中的复杂流程

在上一章中,我们了解了 MobX——React 生态系统中最受欢迎的状态管理库之一。MobX 引入了一些新概念,例如使用状态管理器派生的状态值。其他高级概念与 Redux 相似——例如将状态表示为纯 JavaScript 对象。现在我们将关注 React 状态管理领域的第一个例外:XState。XState 将状态视为一个有限机,而不是一个对象。如果你还没有听说过这个术语,不要担心,我们将在本章的第一节中介绍有限机的话题。

我们将首先探讨 XState 基本理念的理论方面:状态机。然后我们将讨论 XState 的其他高级概念——状态图、动作和 XState 可视化器。当我们对理论感到满意时,我们将在 Funbook 应用中配置 XState,然后我们将实现 XState 以管理应用中的点赞图片。

本章涵盖了以下完整列表:

  • 什么是有限状态机?

  • XState 是什么——高级概念

  • 在 Funbook 应用中配置 XState

  • 使用 XState 为 FavoritedImages 界面

到本章结束时,你将能够理解和使用 XState 作为你项目的状态管理解决方案。你将了解什么是状态机以及它与在其他状态管理库中使用的状态对象有何不同。我希望你也会开始看到你更喜欢使用的解决方案。

技术要求

为了跟随本章的内容,你需要了解一些 JavaScript 和 ReactJS 的知识。如果你至少阅读了本书的 第一章第四章,你应该能够无任何问题地继续前进。

随意使用你选择的 IDE,因为 React Native 不需要任何特定功能。目前,前端开发者中最受欢迎的 IDE 是微软的 VS Code、Atom、Sublime Text 和 WebStorm。

本章提供的代码片段是为了说明我们应该如何使用代码——它们并不提供整个画面。为了更好地跟随编码,请在你的 IDE 中打开 GitHub 仓库并查看其中的文件。你可以从名为 example-app-fullchapter-7 的文件夹中的文件开始。如果你从 example-app-full 开始,你将负责实现本章中描述的解决方案。如果你选择查看 chapter-7,你将看到我实现的整个解决方案。

如果你遇到困难或迷失方向,可以检查 GitHub 仓库中的代码:

github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-7.

什么是有限状态机?

如果我亲爱的读者要求你猜测有限状态机是什么,你可能会说它们与在应用程序中管理状态有关。毕竟,整本书都是关于这个主题的!

有趣的是,有限状态机与应用程序无关;它们与 React 或编程本身都无关。有限状态机是一种计算数学模型。它是一个抽象概念,可以应用于现实生活中的对象或问题,并且代表一个在任何给定时间可以处于有限多个状态之一的状态机。预定义的状态可以根据某些用户输入从一种状态改变到另一种状态。交通灯是一个简单的有限状态机示例:交通灯可以是绿色、红色或黄色,但任何时候都不应该显示两种颜色。另一个简单的状态机示例是电梯。电梯的默认状态是门关闭时静止不动。当用户按下召唤电梯的按钮时,电梯转换到运动状态。当它到达正确的楼层时,它会打开和关闭门。然后电梯回到默认的空闲状态,等待下一个用户输入。

如果你希望了解更多关于这个理论概念的信息,你将在“进一步阅读”部分找到一个关于有限状态机的非常详尽的维基百科页面链接。至于这本书,现在是时候找出我们为什么要讨论这个概念了。你能猜到吗?我敢打赌你能!有限状态机是我们在本章中分析的状态管理库的基本概念:XState。

什么是 XState – 高级概念

现在我们已经掌握了有限状态机的理论概念,我们可以继续讨论 XState 及其主要概念:有限状态机!但这次,我们将从在应用程序中管理全局状态的角度来看待它。

当使用 XState 来管理应用程序的全局状态时,我们应该把我们的状态视为一个有限状态机。这意味着放弃之前将状态表示为普通 JavaScript 对象的概念。在 XState 中,一个组件或一个界面是一个可以处于多个预定义状态之一的机器。让我们考虑用户登录流程。我们的整个应用程序可以处于两种状态之一:用户已登录或用户未登录。我们还需要一个转换机制,让用户可以从一个状态移动到另一个状态。对于主页界面上的图片也是如此。每张图片要么处于“喜欢”状态,要么处于“不喜欢”状态。用户可以通过点击图片下方的爱心图标来改变图片的当前状态。

除了有限状态机之外,XState 还使用了两个其他重要概念:状态图演员模型。状态图基本上是可以用来表示状态机的绘图。以下是一个表示灯泡状态和转换的状态图示例:

图 7.1 – 简单状态图绘制灯开关

图 7.1 – 简单状态图绘制灯开关

上述图示是一个非常简单的状态机。当在移动应用上工作时,您可能会发现自己正在处理更复杂的状态机。从一个非常简单的事物,比如一个表单开始,您可能会在多个元素上添加多个状态,例如启用/禁用、有效/无效和清洁/脏。没有状态图,您将面临状态爆炸。虽然听起来很有趣,但在应用中面对这种情况并不好。让我们看看使用状态转换绘制出的复杂输入示例:

图 7.2 – 复杂状态图

图 7.2 – 复杂状态图

用户点击一个有效的输入并进入有效启用未更改状态。应用会自动过渡到无效启用未更改状态。当用户提供一些输入时,应用将处于无效启用更改状态。如果用户提供的输入有效,我们将进入有效启用更改状态;如果不有效,我们将返回到无效启用更改状态。如果用户在表单中点击其他内容——比如说,一个禁用第一个输入的单选框?我们将进入无效(或有效禁用更改状态。对这个图表进行推理相当困难。这就是状态图特性发挥作用的时候。状态图提供了并行状态、层次结构和守卫的实现。您可以在 XState 文档中推荐的这篇文档中了解更多关于这些概念的信息:statecharts.dev/state-machine-state-explosion.html

XState 背后的最后一个重要概念是演员模型。这是一个计算数学模型,表明一切都是一个“演员”,并且可以执行三件事情:接收消息、发送消息以及处理接收到的消息。

我非常幸运能够就 XState 库的主题向其作者大卫·库尔希德提出几个问题。他告诉我他“创建 XState 有两个原因:管理和可视化复杂逻辑。状态机和状态图是视觉形式化工具,擅长以直观的方式表示甚至是最复杂的流程和逻辑,并且我希望在 JavaScript 应用中使用它们的方式简单。”他还补充说,XState 的高级理念受到了万维网联盟W3C状态图 XMLSCXML)规范的强烈影响。

让我们快速了解一下 SCXML 是什么,以及它为什么有一个 W3C 规范的含义。根据你在编程方面的经验,你可能已经听说过 可扩展标记语言XML)的文件格式和标记语言。XML 用于存储、传输和重建数据。当正确缩进和格式化时,XML 文件易于阅读,因为它们只是描述数据。SCXML 是 XML 的一个堂兄弟。它是一种基于 XML 的标记语言,用于提供基于状态机的环境。它有一个 W3C 规范的事实意味着它可以有信心地用于各种与互联网相关的程序。你可以在 进一步阅读 部分找到整个 W3C 规范的链接。

回到 XState,它不仅受到了 SCXML 的影响,而且与 SCXML 完全兼容,这意味着你可以编写一个描述状态的 SCXML 文档,并且它将与你的 React Native 应用中的 XState 实现一起工作。你还可以用 JavaScript 编写它。无论什么让你感到兴奋!

我向 David Khourshid 询问了他库的未来。XState 是一个开源项目,就像我们在本书中讨论的所有其他状态管理库一样。David 说维护 XState 和开发与 XState 相关的工具是他的全职工作。他正在为 XState 可视化器开发新的强大协作编辑工具。他说:“XState 的下一个主要版本(版本 5)将拥有更多功能,更加模块化,并将“演员”作为一等公民。演员是可以发送和接收消息的实体,状态机只是演员可以拥有的许多行为之一。你还可以将演员表示为承诺、可观察对象、reducer 等,这将允许开发者使用 XState 的 API(和可视化工具)来处理所有逻辑,而不仅仅是 状态机特定的逻辑。”

你可能在前一段中注意到了对 XState 可视化器的提及。这个工具是使 XState 与其他状态管理库截然不同的东西。多亏了这个可视化器,你可以在应用中看到状态和状态之间转换的图形表示。你可以用它来规划新的应用或调试你正在工作的应用。你可以在 xstate.js.org/viz/ 找到这个可视化器。以下是一个示例屏幕截图,展示了它的样子:

图 7.3 – XState 可视化器的屏幕截图

图 7.3 – XState 可视化器的屏幕截图

大卫说,可视化器是他工作过的最难的事情之一。它始终处于进行中,已经经历了多次迭代。目前,它是一个“基于 SVG 的 '画布',内部包含 HTML。" 尽管现在它有一定的交互性——你可以点击转换并观察状态如何变化——大卫说,“使其交互式是另一个难度层,特别是对于拖放交互和修改状态图。" 我个人对可视化器的最新版本非常兴奋。它已经多次帮助我规划我为应用(使用 XState)设计的最佳状态机。

在本节中,我们讨论了 XState 背后的主要思想。它们与我们之前分析的所有方法都不同。整个库基于有限状态机的数学概念。它还使用了状态图和演员模型背后的理论,以确保在复杂应用中管理状态可以有效地进行。现在,是时候看看这个库的实际应用了。让我们继续在 Funbook 应用中实现 XState。

在 Funbook 应用中配置 XState

让我们看看在真实应用中使用 XState 需要什么。如果你想亲自跟随,你可以复制 example-app-full 文件夹并将其用作起点。如果你想查看与本章相关的代码,请查看 chapter-7 文件夹:github.com/PacktPublis…

首先——我们需要将 XState 添加到项目中。你可以通过运行以下两个命令之一来实现:

npm install xstate@latest --save
// or
yarn add xstate@latest --save

XState 本身是一个无偏见的库,就像 MobX 一样。这意味着它不是直接与 React 一起工作的。XState 文档有一个名为 Recipes 的部分,你可以在这里阅读有关使用 React 或其他 UI 库(如 Vue 或 Svelte)的实现。至于我们,我们需要添加与 React 相关的依赖项,xstate-react。让我们通过运行以下两个命令之一来实现:

npm install xstate-react@latest –-save
// or
yarn add xstate-react@latest –-save

现在我们有了准备好的依赖项,让我们运行应用以确保一切按预期工作。如果一切正常,我们可以创建我们的第一个状态机。我们将从一个简单的例子开始:用户登录流程。在高层面上,这个流程中涉及的逻辑并不多。用户可以是登录或注销,他们从一个状态过渡到另一个状态,然后再返回:

import { createMachine } from 'xstate';
export const userFlowMachine = createMachine({
  id: 'userFlow',
  initial: 'anonymous',
  states: {
    anonymous,
    authenticated,
  }
});

阅读代码相当逻辑。我们首先导入一个 createMachine 函数,然后调用它来创建我们的 userFlowMachine 实例。在 userFlowMachine 中,我们首先定义机器 ID 和初始状态。然后我们继续定义应用的两个可能状态。在我们的应用中,用户可以是匿名或认证的。但用户如何从一种状态过渡到另一种状态呢?让我们将这个功能添加到状态机中:

import { createMachine } from 'xstate';
export const userFlowMachine = createMachine({
  id: 'userFlow',
  initial: 'anonymous',
  states: {
    anonymous: {
      on: {
        LOGIN: { target: 'authenticated' },
      }
    },
    authenticated: {
        on: {
            LOGOUT: { target: 'anonymous' },
          }
    },
  }
});

太好了!现在,用户可以处于 anonymous 状态,他们可以通过 LOGIN 转换来进入这个状态。在这个时候,他们将处于 authenticated 状态,他们可以通过 LOGOUT 转换来退出这个状态。你可以继续改进这个例子,通过添加一些 LOGINLOGOUT 转换的实现细节,或者可能是一个错误状态。但我现在将停止讨论这个特定的状态机,看看它应该如何在 React 应用中使用。

毫不奇怪,XState 文档建议使用 React Context 来管理 XState 的全局状态。幸运的是,我们现在对 React Context 已经有了很好的掌握,对吧?那么,让我们看看 XState 文档中的一个 React Context 示例:

import React, { createContext } from 'react';
import { useInterpret } from '@xstate/react';
import { userFlowMachine } from './machines/userFlowMachine;
export const GlobalStateContext = createContext({});
export const GlobalStateProvider = (props) => {
  const userFlowService = useInterpret(userFlowMachine);
  return (
    <GlobalStateContext.Provider value={{ userFlowService }}>
      {props.children}
    </GlobalStateContext.Provider>
  );
};

嗯……这个 useInterpret() 函数是什么?它是从 xstate-react 导入的,是一个特殊的工具,用来确保在使用 React Context 时不会引起过多的重新渲染。useInterpret() 返回一个服务,即状态机的引用。根据 XState 文档:“这个值永远不会改变,所以我们不需要担心 浪费的重新渲染。”

了解你的工具

每个工具都是基于如何使用它的想法而创造的。你可以拿一把锤子用木柄敲钉子,但你已经知道这不是锤子最好的使用方式。同样的规则也适用于 JavaScript 库。没有人天生就知道 JavaScript 库和工具。我们都必须阅读文档并学习我们工具的最佳实践。

我们有创建上下文的方法,现在,让我们看看 XState 使用说明。我们将需要订阅在应用根目录中定义的全局上下文服务。这样的订阅看起来是这样的:

import React, { useContext } from 'react';
import { GlobalStateContext } from './globalState';
import { useActor } from '@xstate/react';
export const SomeComponent = (props) => {
  const globalServices = useContext(GlobalStateContext);
  const [state] = useActor(globalServices. userFlowService);
    return state.matches('loggedIn') ? 'Logged In' :      'Logged Out';
};

我们已经在 React Native 应用中完成了 XState 的基本设置。现在有许多路径可以选择:提高性能、分发事件或使用状态选择器。我们将在下一节中介绍必要的步骤,我们将为 LikedImages 表面和负责添加喜欢图片的模态设置 XState。

使用 XState 处理 FavoritedImages 表面

在上一节中,我们设置了一个基本的机器,可以用来控制应用中的用户流程。现在让我们添加一个新的机器,用于我们的实际应用场景:在一个社交媒体克隆应用中喜欢图片。

我们将首先创建一个最小可行产品MVP)的机器:

// src/machines/likeImagesMachine.js
import { createMachine } from "xstate";
export const likeImagesMachine = createMachine({
  id: "likeImagesMachine",
  context: {
    likedImages: [
       { Example Image Object 1},
       { Example Image Object 2}
       …
       ],
  },
  initial: "loading",
  states: {
    loading: {},
    ready: {},
    error: {},
  },
});

让我们从代码的顶部开始分析:我们首先导入createMachine函数,我们在likeImagesMachine函数的第一行使用它。我们设置机器的 ID 和上下文。记住,XState 上下文与 React 上下文不同。我们谈论了很多关于 ReactJS 上下文的内容;我们知道它可以用于在组件之间共享状态。XState 上下文是一个用于定量数据(如字符串、数组或对象)的容器,这些数据可能无限。喜欢的图片数组是这种数据的绝佳例子,这就是为什么我们将保持这个数组在我们的机器上下文中。为了测试目的,我们将向上下文中的默认likedImages数组添加一些图片。剩下的只是定义机器的状态和设置默认状态。简单易懂!

我们将首先创建和配置一个用于状态的包装器,借助 React 的上下文。一旦使用模拟数据正确设置好一切,我们将从我们的后端获取真实数据。获取数据后,我们将编写最后一段代码:使用 XState 管理喜欢的图片。

配置上下文和组件

现在是时候讨论第一种上下文类型了:React 上下文。我们在上一节中设置了一个巧妙的使用用户流程的上下文。我们将在这个上下文中添加喜欢的图片机器:

// src/context.js
[…]
import { useInterpret } from "@xstate/react";
  import { likeImagesMachine } from "./machines/    likeImagesMachine ";
import { userFlow } from "./machines/userFlowMachine";
export const GlobalStateContext = createContext({});
export const useXStateContext = () => {
  const context = React.useContext(GlobalStateContext);
  if (context === undefined) {
    throw new Error(
        " useXStateContext must be used within a           GlobalStateContextProvider"
    );
  }
  return context;
};
export const GlobalStateProvider = (props) => {
  const likedImagesAppService = useInterpret(likeImagesMachine);
  const userFlowService = useInterpret(userFlow);
  const mergedServices = {
    likedImagesAppService,
    userFlowService,
  };
  return (
    <GlobalStateContext.Provider value={mergedServices}>
      {props.children}
    </GlobalStateContext.Provider>
  );
};

这是个很好的时机来改进我们在本章前一部分更理论性的部分中设置的基本上下文。我们将通过添加一个名为useXStateContext的新自定义钩子来实现这一点。在之前的章节中,我们讨论了使用自定义钩子和 React 上下文是最佳实践。在GlobalStateProvider函数中,我们通过 XState 提供的useInterpret自定义钩子添加了likedImagesMachine。我们将解释的机器合并并作为上下文值传递。上下文值的最后一部分是将组件包装在上下文中。我们必须将全局状态保持在应用的最顶层,以便FavoritedImages界面和ImageDetailsModal都能访问它。以下是你App.js的大致样子:

// src/App.js
[…]
import {
  […]
    GlobalStateProvider
      } from "./src/context";
[…]
return (
    <SafeAreaProvider>
      <GlobalStateProvider>
        <UserStateContext.Provider value={userLoggedIn}>
[…]

让我们使用这个全新的机器,由 React 上下文解释,并在其自己的上下文中持有一些示例图片,在FavoritedImages界面中使用。喜欢的图片列表在ListOfFavorites组件中渲染,这就是我们将要更改的组件:

// src/components/ListOfFavorties.js
import { useXStateContext } from "../context";
import { useActor } from "@xstate/react";
export const ListOfFavorites = ({ navigation }) => {
  const globalServices = useXStateContext();
    const [state] = useActor(globalServices.      likedImagesAppService);
    const [imageData, updateImageData] = useState       (state.context.likedImages);
//…
  return (
    //…
      <FlatList
        data={imageData}
//…

我们首先导入我们创建的用于轻松消费 React 上下文的自定义useXStateContext钩子。我们需要导入的第二件事是 XState 的useActor钩子。这是一个 React 钩子,它订阅来自给定解释状态机的发出的更改,由 XState 作者命名为“actor”。如果你访问 XState 文档,你将找到其他useActor函数的实现,这些实现针对 Svelte、Vue 和其他库进行了定制。这是因为 XState,就像 MobX 一样,在 UI 库方面持中立态度。

最后,我们需要在我们的组件中使用所有这些导入的项目。我们从 React 上下文中拉取数据,并通过useActor钩子订阅变化。我们可以直接使用useActor钩子返回的状态。然而,React Native 的FlatList需要非常清楚地了解数据变化以便更新。因此,我添加了一个useState钩子,包括updateImageData设置函数,一旦我们尝试动态地向此数组添加图像,它将非常有用。

说到动态性,是时候考虑通过 XState 进行数据获取了。但在我们继续之前,请确保使用当前更改运行您的应用程序,并确保您可以在FavoritedImages界面上看到likeImagesMachine函数的示例图像。如果您遇到任何错误,您可以查看您的终端窗口,因为许多 XState 错误都会在那里描述。它们也应该在您的手机模拟器或物理设备上可见。以下是在控制台和模拟器中同时可能看到的示例错误:

图 7.4 – 控制台和手机模拟器中的 XState 错误图 7.4 – 控制台和手机模拟器中的 XState 错误

图 7.4 – 控制台和手机模拟器中的 XState 错误

获取图像数据

获取数据并不总是状态管理库的强项。毕竟,这并不是它们的基本职责。然而,在 XState 的情况下,获取数据却非常自然,因为每个 Promise 都可以被建模为一个状态机。从高层次来看,我们需要启动一个将处于默认的“加载”状态的功能。我们将等待它发生某些事情——要么解决要么拒绝——然后进入适当的“已解决”或“已拒绝”状态。以下是我们图像获取机器的构建过程:

// src/machines/fetchMachine.js
import { createMachine, assign } from "xstate";
export const fetchImagesMachine = createMachine({
  id: "fetchImages",
  initial: "loading",
  context: {
    retries: 0,
    images: [],
  },
  states: {
    loading: {
      on: {
        RESOLVE: "success",
        REJECT: "failure",
      },
    },
    success: {
      type: "final",
    },
    failure: {
      on: {
        RETRY: {
          target: "loading",
          actions: assign({
            retries: (context, event) => context.retries+1,
          }),
        },
      },
    },
  },
});

您在这里看到的是一个非常简单的机器,准备描述从外部源获取数据的过程。我们有三个状态:初始的“加载”状态,以及“成功”和“失败”状态。您可以看到在“加载”状态中有两个动作,可以用来管理获取机制。在“失败”状态中还有一个“重试”动作。我们可以在应用程序中使用它,让用户在发生错误时手动尝试获取数据。就基本设置而言,这都很好,但我们需要了解如何调用实际的端点。为了做到这一点,我们将改变“加载”状态:

//…
states: {
    loading: {
      invoke: {
        id: 'fetchImagesFunction',
        src: async () => {
          const response = await fetch(
            requestBase + "/john_doe/likedImages.json"
          );
          const imageData = await response.json();
          return imageData;
        },
        onDone: {
          target: "success",
          actions: assign((context, event) => {
            return {
              images: event.data,
            };
          }),
        },
        onError: {
          target: "failure",
          actions: assign({
              error: (context, event) => "Oops!                Something went wrong",
          }),
        },
      },
    },

为了代替可能需要手动调用的两个动作,我在loading状态中添加了invoke属性。这样,当机器被创建时,图片将自动加载,无需用户交互。invoke属性的值是一个包含要调用的函数的idsrc属性的对象。可以调用 Promise、回调(可以发送和接收来自父机器的事件)——可以发送事件到父机器——以及整个机器。我们将保持简单,并在源中添加一个异步的fetch函数。你还可以在任何机器外部创建一个命名函数,并通过src调用它。我们还使用了invoke属性的两个可选值:onDoneonError。这两个转换在处理 Promise 时非常有用。它们像任何其他 XState 转换一样——包括动作和目标状态。两个动作都包含assign关键字。assign是一个更新机器上下文的函数。我们在这里使用它来将获取到的结果数据传递到上下文,以便我们可以在应用程序的后续操作中使用它。分配器函数有一些注意事项:它们必须是纯函数,并且必须遵循严格的顺序。如果你想了解更多关于它们的信息,请查看进一步阅读部分提供的链接。

如果一切顺利,你应该能够通过这个功能获取图片。但我们在likeImagesMachine函数中如何使用这些图片呢?记得我们刚才用过的invoke属性吗?我们将在likeImagesMachine的加载状态下使用相同的属性来调用这个获取机器,并通过onDone函数传递获取到的数据:

// src/machines/likeImagesMachine.js
import { fetchImagesMachine } from "./fetchImagesMachine";
export const likeImagesMachine  = createMachine({
  id: "likeImagesMachine ",
  context: {
    likedImages: [],
    currentImage: null,
  },
  initial: "loading",
  states: {
    loading: {
      invoke: {
        id: "fetchImagesMachine",
        src: fetchImagesMachine,
        onDone: {
          target: "ready",
          actions: assign({
            likedImages: (context, event) => {
              return event.data.images;
            },
          }),
        },
      },
    },
//…

在这个代码片段中,我们导入了fetchImagesMachine函数,并在likeImagesMachine函数的加载状态下调用它。让我们更仔细地看看我们用来从fetchImagesMachine传递图像数据到这个父机器的分配器函数。它有一个onDone函数,当fetchImagesMachine达到其最终状态时将被调用。这个函数将调用机器返回的数据分配给likeImagesMachinecontext,并通过event传递数据。你会注意到我们正在调用event.data.images。这从哪里来的?这是我们需要在fetchImagesMachine中添加的东西。到目前为止,该机器只将其获取到的数据传递到其context,但我们需要将其公开,以便父机器likeImagesMachine可以访问它。我们已经知道在父机器(likeImagesMachine)中,当子机器(fetchImagesMachine)达到其最终状态时,会调用onDone事件。在我们的例子中,最终状态是success。这就是我们可以添加data属性的地方:

// src/machines/fetchImagesMachine.js
//…
success: {
      type: "final",
      data: {
        images: (context, event) => context.images,
      },
    },
//…

这段代码告诉 fetchImagesMachine 函数将其最终状态添加一个 data 对象。这是我们运行父级 likeImagesMachine 中的 onDone 时访问的对象。如果一切顺利,你现在应该能在你的应用程序中看到获取到的所有图像数组。这是一个在设备或模拟器上运行应用程序的好时机,如果你还没有这样做的话。

管理图像模态中的图像

我们已经有一个很好的设置——我们在获取图像并将它们提供给应用程序。不过,我们的应用程序相当静态。我们需要一种方法来向喜欢的图像数组中添加新图像。我们还希望检查图像是否被点赞,以便在 ImageDetailsModal 中显示适当的图标。

如果我们想知道图像是否应该被点赞或取消点赞,我们首先需要知道它是否已被点赞。但即使在我们知道图像是否已被点赞之前,我们还需要知道与该图像相关的所有数据。我们将在 likeImagesMachine 机器的上下文中添加一个新项目——currentImage

export const likeImagesMachine  = createMachine({
  id: "likeImagesMachine ",
  context: {
    likedImages: [],
    currentImage: null,
  },
//…

这是我们将存储当前查看图像信息的地方。上下文初始化为 null,我们需要添加一个将更新此上下文值的动作。我们将在 likeImagesMachineready 状态中添加一个名为 MODAL_OPEN 的新事件:

// src/machines/likeImagesMachine
ready: {
      on: {
        MODAL_OPEN: {
          actions: assign((context, event) => {
            return {
              currentImage: event.payload,
            };
          }),
        },
        MODAL_CLOSE: {
          actions: assign((context, event) => {
            return {
              currentImage: null,
            };
          }),
        },
      },
//…

ImageDetailsModal 打开时,我们将调用 MODAL_OPEN 动作,当模态关闭时调用 MODAL_CLOSE——非常直接!您可以在以下链接中看到代码的实际应用:

// src/surfaces/ImageDetailsModal.js
export const ImageDetailsModal = ({ navigation, route }) => {
  const globalServices = useXStateContext();
  const { send } = globalServices.likedImagesAppService;
  useEffect(() => {
    send({
      type: "MODAL_OPEN",
      payload: route.params.imageItem,
    });
    return () => {
      send("MODAL_CLOSE", {});
    };
  }, []);

我们首先使用一个名为 useXStateContext 的自定义钩子来消费我们之前设置的上下文值。然后,我们使用来自 likedImagesAppServicesend 函数。最后,我添加了一个 useEffect 钩子,当模态渲染时调用 MODAL_OPEN 动作,并将 MODAL_CLOSE 作为清理函数。

现在我们已经将当前图像保存在机器上下文中,我们可以检查它是否受欢迎。为此,我们将使用来自 XState 的另一个实用工具:一个名为 useSelector 的自定义钩子。选择器这个名称可能对你来说很熟悉。在 JavaScript 中,有查询选择器,Redux 推崇使用选择器函数,还有 CSS 选择器。XState 选择器在意识形态上与 Redux 中的选择器最为接近。它们是特殊的函数,接收当前状态并根据某些条件返回一个值。我们的当前状态是图像数组以及当前图像,条件是当前图像是否在图像数组中。代码在下面的代码片段中展示:

const isImageLikedSelector = (state) => {
  if (!state.context.currentImage) {
    return;
  }
  const checkIfInImagesArray = state.context.likedImages.find(
      (image) => image.itemId === state.context.currentImage.        itemId
  );
  return !!checkIfInImagesArray;
};

如前所述,这个选择器将接收当前状态作为第一个参数。我们首先检查图片数组不是 null。我们在该数组上运行 find 函数,如果它是 nullundefined,这会导致应用程序崩溃。一旦我们确定图片数组存在,我们就可以通过当前图片过滤它。你可以把这个函数放在任何你想放的地方(与机器相同的文件中,在名为 selectorsutilities 的文件中,等等),然后将其导入到 ImageDetailsModal

// src/surfaces/ImageDetailsModal.js
export const ImageDetailsModal = ({ navigation, route }) => {
  const globalServices = useXStateContext();
  const { send } = globalServices.globalAppService;
  const isImageLiked = useSelector(
    globalServices.globalAppService,
    isImageLikedSelector
  );

isImageLiked 常量可以在组件中使用,以检查应该显示哪个图标以及应该调用哪个动作(点赞或取消点赞)。

点赞图片

我们的状态机了解我们已获取并显示在 FavoritedImages 表面上的图片数组。它们也通过 MODAL_OPEN 动作了解到当前查看的图片。现在,我们需要告诉它们如果有人按下“点赞”按钮应该怎么做。让我们向 likeImagesMachine 函数添加一个新的动作:

// src/machines/likeImagesMachine.js
//…
ready: {
      on: {
        LIKE: {
          actions: assign((context, event) => {
            const updateImageArray = event.payload.concat(context.likedImages);
            return {
              likedImages: updateImageArray,
            };
          }),
        },
//…

我们正在使用之前遇到过的分配器函数。在其内部,我们将只包含当前图片的数组连接到所有图片的完整数组。这样,新添加的图片就会位于数组的顶部,并在 FlatList 的顶部。现在,动作已经准备好了,我们可以在模态中调用它,如下所示:

// src/surfaces/ImageDetailsModal
//…
<Pressable
          onPress={() => {
            if (!isImageLiked) {
                send({ type: "LIKE", payload:                  [route.params.imageItem] });
            }
//…

我们已经做了很多更改——让我们在我们的应用程序中测试它们。如果你一直跟着做,你应该能看到获取的图片在 FavoritedImages 表面上正确加载。ImageDetails 模态也正确打开,显示已点赞图片的完整心形,未点赞图片(在 Feed 表面上)显示为空心形。我们甚至可以按下空心形,它会变成实心!点赞动作和选择器按预期工作!太棒了!

很不幸,FlatList 有点固执。正如之前提到的,FlatList 需要显式的数据更改才能重新渲染,而如果我们想看到新添加的图片,我们就需要它重新渲染。我们不得不稍微“扭动它的手”,通过添加这个 useEffect 钩子:

// src/components/ListOfFavorites
export const ListOfFavorites = ({ navigation }) => {
  const globalServices = useXStateContext();
  const [state] = useActor(globalServices.globalAppService);
  const [imageData, updateImageData] = useState([]);
  useEffect(() => {
    updateImageData(state.context.likedImages);
  }, [state.context.likedImages]);
//…

现在,一切应该都能完美工作!是时候给自己鼓掌了!在本节中,我们涵盖了大量的主题。我们讨论了多个状态机的实际应用,调用获取函数,在机器之间传递上下文值,调用动作和使用选择器。有了这些知识,你应该能够配置任何应用程序以使用 XState 作为状态管理库。

摘要

XState 是本书中第一个基于数学原理的基本状态管理库。我们简要地讨论了这些原理,因为理解它们对于理解 XState 非常有用。最重要的概念是状态机。在数学的世界里,它们并不新鲜;然而,当我们谈到移动应用中的全局状态时,它们却相当新颖。一旦我们掌握了理论,并发现了非常有用的 XState 可视化工具,我们就准备好进行实际工作了。我们在 Funbook 应用中设置了 XState,使用了 XState 文档中描述的最佳实践。我们探讨了将 XState 作为全局状态解决方案来管理点赞图片用例的实现。我们研究了使用 XState 获取数据和更改数据。我希望你们喜欢它!现在,是时候继续我们的旅程,探索状态管理库世界中的下一个异常值:Jotai

进一步阅读

第八章:在 React Native 应用中集成 Jotai

在上一章中,我们探索了XState的数学世界。我们将继续我们的旅程,通过探索另一个名为Jotai的年轻状态管理库来继续前进。Jotai受到了在Facebook创建的一个实验性状态管理库 Recoil 的启发。在本章中,我们将简要了解Recoil,这是 Facebook 创建的一个实验性状态管理库。一旦我们熟悉了这个库的主要思想,即一个名为“原子状态”的新概念,我们将深入探讨Jotai。我们将在我们的应用中配置 Jotai,并借助Jotai继续进行数据获取和管理点赞图片的工作。以下是本章我们将涉及的内容:

  • Recoil和原子状态是什么?

  • 什么是Jotai

  • 在 Funbook 应用中配置Jotai

  • 使用FavoritedImages

到本章结束时,你将有一种新的看待全局状态管理的方法——通过将其划分为称为原子的小项目。你还将了解如何在新的项目中设置Jotai,以及如何使用它进行数据获取和数据管理。

技术要求

为了跟上本章的内容,你需要了解一些JavaScriptReactJS的知识。如果你至少跟随着本书的第一章到第四章,你应该能够没有问题地继续学习。

随意使用你选择的 IDE,因为React Native不需要任何特定功能。目前,前端开发者中最受欢迎的 IDE 是微软的 VSCode、Atom、Sublime Text 和 WebStorm。

本章提供的代码片段旨在说明我们应该如何使用代码。它们并不提供完整的画面。为了更容易地编码,请在你选择的 IDE 中打开 GitHub 仓库,查看其中的文件。你可以从名为example-app-fullchapter-8的文件夹中的文件开始。如果你从example-app-full开始,你将负责实现本章描述的解决方案。如果你选择查看chapter-8,你将看到我实现的整个解决方案。

如果你遇到困难或迷失方向,可以查看 GitHub 仓库中的代码:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-8

Recoil和原子状态是什么?

如果你一直按章节顺序阅读这本书,你可能觉得不同类型的状态管理库列表永远没有尽头。在某种程度上,这是正确的。每隔几周就会出现新的状态管理库;它们有时是纯开源的,有时是公司支持的。然而,它们很少提出突破性的解决方案。更常见的是,它们是已知概念的较新实现。这些实现受到了极大的欢迎,因为每个开发者都喜欢舒适地工作——那么,那些已知的概念是什么呢,你可能想知道?

ReactJS世界中,有一个共识,即状态管理库可以分为三种类型:

  1. Flux 类型 - 这些是持有状态在组件之外的状态管理库,并使用单向数据流。它们受到了Facebook 的 Flux的启发,最著名的例子是Redux。这种流有现代的实现,如Redux ToolkitZustand

  2. 代理类型 - 这些库“包装”状态,概念上类似于代理所做的那样。当使用这种类型的状态管理时,开发者可以像读取组件中的任何其他值一样订阅和读取包装值。代理类型状态管理的最佳例子是React 的 ContextMobXValtio

  3. 原子类型 - 这是最低级别的状态集合,由类组件中的setState和函数组件中的useState钩子自然管理。以这种方式设置的价值可以在应用程序中传递并用于更大的上下文中。Facebook创建了一个实验性库来推广这种类型的状态管理,称为RecoilJotai很快也效仿。

回弹(Recoil)是在 2020 年中期创建的,并迅速获得了大量关注。它是由 Facebook 自己发布的,Facebook 也是 React 的创造者,因此每个人都期待着一个全新的解决方案。使用尽可能小的状态片段,并在React应用中分散和可访问的想法非常吸引人。不幸的是,在最初的兴奋之后,React 社区中很大一部分人对Recoil失去了兴趣,继续使用 Redux 进行日常工作。两年后,Recoil的文档仍然声明它是实验性的,而且很少有人谈论它。

尽管如此,有一小群开发者比我们其他人更加关注。Poimandres,一个开源开发者集体,开始工作并创建了他们自己的原子状态实现。他们称之为Jotai。如果你访问他们的 GitHub 页面,你会看到他们也开发了Valtio,一个代理类型的状态管理库,以及Zustand,一个轻量级的 flux 类型状态管理库。ValtioZustand到目前为止在更著名的替代品阴影之下,但Jotai在原子状态管理舞台上占据了主导地位。这个库是生产就绪的;它正在通过GitHub积极开发,并且其开发者在一个开放的 Discord 服务器上提供持续的支持。这就是为什么我们将讨论Jotai,而不是Recoil,在本章中。

什么是 Jotai?

如前所述,useState)或外部。Daishi 一直在努力开发新事物;你可以在Jotai Labs GitHub仓库github.com/jotai-labs中观察他的所有工作。他对开发用于获取和React 的 Suspense的功能也很感兴趣。你可以在进一步 阅读部分找到更多关于他的项目的链接。

我们现在对为什么创建 Jotai 有了很好的理解。它旨在从新的视角解决状态管理问题,遵循 React 的最佳实践和实验性 Recoil 库提出的概念。是时候在我们应用中尝试这种“原子”状态方法了。让我们开始编码吧!

在 Funbook 应用中配置 Jotai

如果你是一个简单性的粉丝,我亲爱的读者,你可能会爱上这个状态管理库。在我们的应用中配置它只需要在终端运行install命令:

npm install jotai

或者,查看以下内容:

Yarn add jotai

需要添加的一个隐藏的配置宝石是 Suspense。我特意用了宝石这个词,因为Jotai的这项配置要求将使你的应用崩溃更少。Suspense 是 ReactJS 的新功能,旨在只能渲染准备好渲染的组件。就像任何新功能一样,用户需要习惯它,有时甚至需要被迫尝试它。Jotai正是这样做的:强迫用户使用 Suspense,这对他们自己是有好处的!让我们继续在我们的应用根目录中添加它:

// ./App.js
import React, { useState, Suspense } from "react";
export default function App() {
//…
  if (!fontsLoaded) {
    return <AppLoading />;
  }
  return (
    <SafeAreaProvider>
//…
            <Suspense fallback={<AppLoading />}>
              <NavigationContainer theme={MyTheme}>
                <Stack.Navigator>
//…

现在,我们的应用可以使用ListOfFavoritedImages

使用 Jotai 进行 ListOfFavoritedImages

你可能注意到我们没有对Jotai进行太多的理论介绍。这是因为这个库非常简洁。没有样板代码,没有复杂的概念。我们只需要创建一个原子并使用它,多亏了应用中的自定义钩子。让我们先创建一个带有一些模拟数据的喜欢图片原子的例子:

// src/atoms/imagesAtoms.js
import { atom } from "jotai";
export const imageListAtom = atom([
  {
    "itemId": 1,
    "authorId": 11,
    "timeStamp": "2 hrs ago",
    "url": "…",
    "likes": "28",
    "conversations": "12"
  },
  {
    "itemId": 2,
    "authorId": 7,
    "timeStamp": "1 week ago",
    "url": "…",
    "likes": "8",
    "conversations": "123"
  },
]);

我们已经有了模拟的图像数组;我们现在需要做的就是使用它。鉴于我们之前与其他状态管理库的经验,你可能期望看到某种设置、包装器、订阅或其他类似的东西。很抱歉让你失望,但我们现在需要做的只是如下使用ListOfFavoritedImages组件:

import { useAtom } from "jotai";
import { imageListAtom } from "../atoms/imagesAtoms";
export const ListOfFavorites = ({ navigation }) => {
  const [imageList] = useAtom(imageListAtom);
  if (!imageList) {
    return <AppLoading />;
  }
//…
  return (
    //…
      <FlatList
        data={imageList}
//…

在前面的代码中,我们导入了useAtom以及我们在imagesAtom文件中创建的原子。那么结果如何呢?让我们在模拟器中运行应用并找出答案!

图 8.1 – 基于 Jotai 原子的应用显示图像

图 8.1 – 基于 Jotai 原子的应用显示图像

一切都正常!我必须承认,这感觉几乎像魔法。当然,获取数据会更复杂吗?

使用 Jotai 获取数据

我们在我们的应用中成功设置了模拟的图像数据,但我们希望从服务器获取真实数据。回到Jotai文档,我们将找到一个关于异步原子的指南(你可以在进一步阅读部分找到该文档的链接)。以下是我们的用于获取图像的异步原子的样子:

// src/atoms/imageAtoms.js
import { requestBase } from "../utils/constants";
import { atom } from "jotai";
export const imageListAtom = atom([]);
  const urlAtom = atom(requestBase + "/john_doe/likedImages.    json");
export const fetchImagesAtom = atom(async (get) => {
  const response = await fetch(get(urlAtom));
  return await response.json();
});

我们添加了requestBase导入以更舒适地使用 URL。然后,我们继续创建一个具有特定 URL 的基本原子。最后一个函数是异步原子。我们知道它是异步的,因为它使用了async关键字。异步原子函数的主体是一个fetch函数和数据返回。原子已经准备好了,但还没有连接到任何东西。我们需要在应用中调用它,并让它填充imageListAtom。让我们从调用获取操作开始。这样做的好地方是在用户登录后应用根目录。这意味着我们不会在App.js根组件中获取数据,而是在Home组件中:

// src/surfaces/Home.js
import { useAtom } from "jotai";
import { fetchImagesAtom } from "../atoms/imageAtoms";
//…
export const Home = () => {
  const [json] = useAtom(fetchImagesAtom);

我们首先导入必要的部分:从console.log到组件的自定义钩子,并查看json的值是否与预期相同。顺便说一句,原子返回的命名没有规则。你也可以这样写:

  const [thisIsAVeryFancyAndCuteFetchingMechanism] =    useAtom(fetchImagesAtom);

如果你使用 linter 插件(例如json值被声明但未使用。如果我们不对它们做任何事情,获取图像有什么好处?我们应该如何处理它们?我们应该让新获取的图像数组填充imageListAtom。完成这个任务的方法是将我们的只读imageListAtom改为读写原子。

读取和写入原子

啊!终于有一些理论了!我敢肯定,亲爱的读者,你一定渴望这些!(由于在技术文本中传达讽刺很难,让我抓住这个机会解释一下:上一句话是讽刺的)。

原子有三种类型:只读、只写和读写原子。只读原子是最简单的:你所做的就是创建它们,并设置它们需要保留的值,例如:

const onlyReadMe = atom('I like to read')

只读原子可以保存比简单的值或字符串更多的内容。如果你需要在原子中实现更复杂的逻辑,你应该使用以下语法:

  const readMeButInUpperCase  = atom((get) =>    get(onlyReadMe).toUpperCase())

在前一个简短的代码片段中,你可以观察到原子可以访问一个getter函数,而这个函数反过来又可以访问其他原子。

如果我们想要给我们的原子添加写功能,我们可以在原子的第二个参数中添加一个setter函数:

const readMeButInUpperCase  = atom(
      (get) => get(onlyReadMe).toUpperCase(),
      (get, set, newText) => {
          set(onlyReadMe, newText)
       }
)

我们添加了一个新函数,它将接受一个新的文本并将其传递给onlyReadMe原子。如果你要在组件中使用它,它看起来会是这样:

const FancyTextComponent = () => {
    const [fancyText, setFancyText] =      useAtom(readMeButInUpperCase  );
return (
      <Pressable onPress={() => setFancyText         ('I do not like to swim')>
        <Text>Likes and dislikes: {fancyText}</Text>
    </Pressable>
)

在前一个截图中的示例组件中,你可以观察到如何实现读写原子。我们首先导入原子,但声明了两个值:值和设置器,这与我们在常规useState钩子中使用的方法非常相似。在组件的较低部分,我们使用{fancyText}来显示原子中的文本,并使用setFancyText函数通过按钮点击来设置新的文本。

我们可以讨论的最后一种原子是只写原子。这种原子与读写原子的唯一区别在于我们声明读取参数为null。以下是一个示例:

const onlyUsedForSettingValues  = atom(null,
       (get, set) => {
           set(onlyReadMe, 'I like using write only atoms')
       }
)

当使用这种类型的原子时,你总是需要确保为非存在的默认值适配钩子。以下是如何在前面的示例组件中使用这个只写钩子的方法:

const FancyTextComponent = () => {
const [readOnlyFancyText] = useAtom(onlyReadMe);
    const [, setStaticText] =      useAtom(onlyUsedForSettingValues  );
return (
    <Pressable onPress={() => setFancyText()>
        <Text>Likes and dislikes: { readOnlyFancyText }</Text>
    </Pressable>
)

注意到数组中有来自useAtom钩子的值,其中的逗号表示第一个索引处有一个空值,但我们选择不使用它。

向 imageListAtom 添加读写功能

到目前为止,我们有一个只读的imageListAtom和一个异步的fetchImagesAtom。让我们给imageListAtom添加写功能,以便它可以接受来自fetchImagesAtom的值:

// src/atoms/imageAtoms.js
export const imageListAtom = atom([], (get, set, newArray) => {
  set(imageListAtom, newArray);
});

原子已经准备好接收值了,所以让我们给它一些值。我们必须回到启动数据获取的Home组件,并添加一个useEffect,它将更新imageListAtom。以下是代码应该看起来像什么:

// src/surfaces/Home.js
export const Home = () => {
  const [json] = useAtom(fetchImagesAtom);
  const [, setAllImages] = useAtom(imageListAtom);
  useEffect(() => {
    if (json) {
      setAllImages(json);
    }
  }, [json]);

这是个检查应用是否一切正常的好时机,因为我们刚刚实现了数据获取。如果一切确实按预期工作,我们将继续实现console.log的功能,以检查原子是否持有并返回你期望它们拥有的值。如果你继续遇到问题,可以加入Poimandres Discord 服务器(在进一步阅读部分有链接),在那里你可以找到一个Jotai专属频道。Jotai的作者Daishi Kato会亲自回答这个频道上的各种问题。

一旦你确定一切正常,我们将继续实现ImageDetailsModal

实现喜欢按钮

ImageDetailsModal的完整功能由两部分组成:心形图标是否完整——表示图片是否已被喜欢,以及实际喜欢图片的动作——这意味着将新图片添加到收藏表面的图片数组中。

让我们先创建心形图标所需的原子。我们需要知道一个给定的图像是否已被点赞。我们可以通过过滤图像数组并检查给定的图像是否存在于数组中来确定它是否已被点赞。下面是生成的原子将看起来像什么:

// src/atoms/imageAtoms.js
  export const isImageLikedAtom = atom(false,    (get, set, newImage) => {
  const imageList = get(imageListAtom);
  const checkIfLiked =
      imageList?.filter((favoritedImg) => favoritedImg.itemId         === newImage.itemId)
      .length > 0;
  set(isImageLikedAtom, checkIfLiked);
});

根据原子语法,我们首先将默认值设置为false。然后添加一个设置函数,该函数将接收新的图像对象。在设置函数内部,我们使用get函数获取imageListAtom并检查当前的图像对象是否与之匹配。最后,我们将isImageLikedAtom设置为正确的值。一旦原子创建完成,我们就需要在组件中使用它:

// src/surfaces/ImageDetailsModal.js
export const ImageDetailsModal = ({ navigation, route }) => {
    const [isCurrentImageLiked, setIsLiked] =      useAtom(isImageLikedAtom);
  setIsLiked(route.params.imageItem);
//…

你可能想知道为什么我们如此粗糙地调用setIsLiked函数——为什么不使用useEffect?事实上,我们需要这个函数在组件渲染时被调用,并且仅在此之后。我们可以添加一个带有空依赖数组的useEffect钩子,但这会使结果看起来更复杂。

它何时运行?

React 组件的生命周期有一些微妙之处。在类组件中,这些微妙之处更为明显,我们会使用componentDidMountcomponentWillUnmount等。函数组件有相同的生命周期,但没有那么明显。而且,useEffect钩子仅在给定组件完成渲染后运行,而直接调用的函数则不需要等待渲染完成。

就我们的示例而言,我们不需要在调用setIsLiked函数之前确保渲染完成。然而,大型应用程序往往对开发者要求很高,你可能会遇到需要密切控制给定原子设置函数(或任何其他函数)何时运行的情况。你可以在“Further reading”部分的链接中了解更多关于这个主题的信息:“Difference between ‘useEffect’ and calling function directly inside a component”。

回到我们的用例:我们有一个非常好的isImageLiked原子。你可以通过在Feed表面打开图像模态来测试它是否正确工作——那里的心形图标应该是空的——以及在收藏表面——那里的心形图标应该是满的。

现在,让我们转到点赞操作!我们在这里不需要做任何太花哨的事情。我们必须获取imageListAtom并向其中添加一个新图像:

// src/atoms/imageAtoms.js
export const addImageToArray = atom(
         null,
         (get, set, newImage) => {
          const clonedArray = get(imageListAtom);
          clonedArray.unshift(newImage);
          set(imageListAtom, clonedArray);
          set(isImageLikedAtom, newImage);
         }
);

就像示例中的只写原子一样,我们首先声明一个空值作为默认原子值。在设置函数中,我们获取imageListAtom并使用unshift函数添加新图像,该函数将项目添加到原始数组的开头。我们通过将新创建的数组设置为imageListAtom并在isImageLikedAtom中触发设置器来完成。让我们将此添加到模态组件中:

// src/surfaces/ImageDetailsModal.js
export const ImageDetailsModal = ({ navigation, route }) => {
  const [, addImage] = useAtom(addImageToArray);
  const [isCurrentImageLiked, setIsLiked] = useAtom(isImageLikedAtom);
  setIsLiked(route.params.imageItem);
return (
//…
    <Pressable
          onPress={() => {
            if (isCurrentImageLiked) {
              // add remove image functionality here
            } else {
              addImage(route.params.imageItem);
            }
          }}
        >
            <Ionicons name={isCurrentImageLiked ? "heart" :              "heart-outline"} />
        </Pressable>
//…

我们必须将addImageToArray原子导入到我们的组件中,然后在按钮被点击的正确位置调用它。让我们测试我们的应用!很可能会一切正常。你可以点击空的心形图标,它会变成满的,然后关闭模态框并转到FlatList

React Native 的FlatList是一个纯组件,这意味着除非明确指示,否则它不会重新渲染。我们已经在使用FlatList时遇到过这个问题。FlatListextraData属性——我们可以将原子值传递给useState,让自然状态重新渲染组件。我们还可以利用 React Navigation 库提供的工具。这是我最喜欢的方法,也是我选择使用的方法。在Favorites界面中有一个useIsFocused自定义钩子:

// src/surfaces/Favorites.js
import { useIsFocused } from "@react-navigation/native";
export const Favorites = ({ navigation }) => {
  const isFocused = useIsFocused();
  return (
      <SafeAreaView style={{ flex: 1, paddingTop: headerHeight         }}>
      <Suspense fallback={<AppLoading />}>
        <ListOfFavorites navigation={navigation} isFocused={isFocused} />
//…

使用这个钩子,每次这个标签页获得焦点时,Favorites界面都会重新渲染。当然,这是一个需要谨慎使用的钩子。过多的重新渲染会导致应用意外崩溃。如果你决定使用它,请确保重新渲染是必要的。

是时候再次访问 Funbook 应用了!在本节中,我们首先使用了一个基本的钩子和一个模拟的图片数组。然后我们实现了使用ImageDetailsModal的数据获取,并检查你的收藏界面上的图片是否正确更新。

摘要

在本章中,我们介绍了Jotai,这是状态管理库中的新成员。受 Facebook 通过其名为Recoil的库提出的新原子状态管理方法的启发,Jotai在 React 社区中越来越受欢迎。它提供了一种自下而上的方法,与自上而下的库(如ReduxMobX)相反。它确实非常容易配置和使用。它不提供很多工具,但文档非常清晰且易于使用。在本章中,我们成功地使用它来获取和存储数据,我们还用它来实现对数据的操作,例如向数组中添加项目。Jotai标志着我们与经典状态管理库的旅程的结束。

在下一章中,我们将讨论React Query,它不是一个状态管理库,而是一个数据获取库。然而,它在这本书中也有其位置。更多内容将在下一章中介绍!那里见!

进一步阅读

第九章:使用 React Query 进行服务器端驱动状态管理

欢迎您,我亲爱的读者,来到最后一章,本章将描述我们 Funbook 应用的状态管理解决方案。在前一章中,我们探讨了(截至本书编写时)最年轻的状态管理库——Jotai。Jotai 是一个基于 Facebook 团队在他们的开源库Recoil中提出的想法的极简解决方案。React Query同样也是极简的,但意义却大不相同。React Query 是为了在服务器上管理获取和修改数据而创建的。在本章中,我们将探讨 React Query 能提供什么。我们将首先对这个库进行广泛的了解;然后我们将实现它用于数据获取。鉴于我们当前的应用设置,我们没有真实的后端服务器进行通信,所以我们只能从理论上查看数据修改。我们还将查看 React Query 团队为React Native创建的一些专用实用工具。

下面是我们将在本章中涵盖的主题列表:

  • 什么是 React Query,为什么它会在本书中?

  • 安装和配置 React Query

  • 使用 React Query 进行数据获取

  • 其他 React Query 功能

  • React Query 的 React Native 实用工具

到本章结束时,你将很好地理解如何使用 React Query 来提升你的开发体验和代码库。你将掌握如何使用 React Query 处理数据获取,并对该库的其他功能有一般了解。

技术要求

为了跟上本章的内容,你需要具备一些JavaScriptReactJS的知识。如果你已经阅读了本书的至少第一章第四章,你应该能够无任何问题地继续前进。

随意使用你选择的 IDE,因为React Native不需要任何特定功能。目前,前端开发者中最受欢迎的 IDE 包括微软的 VSCode、Atom、Sublime Text 和 WebStorm。

本章提供的代码片段旨在说明我们应该如何使用代码。它们并不提供完整的画面。为了在阅读本章的同时获得更好的编码体验,请在你的 IDE 中打开 GitHub 仓库,查看其中的文件。你可以从名为example-app-fullchapter-9的文件夹中的文件开始。如果你从example-app-full开始,你将负责实现本章中描述的解决方案。如果你选择查看chapter-9,你将看到我实现的整个解决方案。

如果你遇到困难或迷失方向,可以查看 GitHub 仓库中的代码:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-9

什么是 React Query,为什么它会在本书中?

首先,让我们谈谈这个库的名字。在本章中,我使用的是 React Query 这个名字,它也是一个常用的名字。然而,React Query 的创造者 Tanner Linsley 在 2022 年对他拥有的和维护的开源库进行了一些重构。他创建了一个总称,TanStack,并将大量库放在这个名字下。因此,React Query 变成了 TanStack Query,从 React Query 版本 4 开始。你可以在本章末尾的 进一步阅读 部分找到 TanStack 主页的链接。

现在我们已经解决了名字的问题,让我们来谈谈 React Query 在本书中的位置。React Query 不是一个状态管理库。它是一个提供在服务器上舒适地进行数据获取和数据变更的解决方案的库。为什么我们要讨论它呢?因为高效地与服务器通信可以替代任何全局状态管理的需要。鉴于我们现实生活中的社交媒体应用克隆,我们在每一章中都在管理点赞的图片。如果我们每次用户点赞图片时都向服务器发送那个信息,或者当用户访问 FavoritedImages 表面时从服务器拉取列表的最新版本,会怎么样呢?你可能认为:“哇,那会有很多请求!很多加载状态,应用就会变得毫无用处……” 你是对的!除非你使用 React Query。React Query 不仅简化了数据获取,还管理了缓存值、刷新值、后台获取以及更多。

现在我们已经对 React Query 有了一个理论上的理解,我们可以开始编码了。让我们来玩一玩这个非状态管理库。

安装和配置 React Query

安装这个库与其他依赖项没有不同,我们需要运行一个安装脚本。要使用 npm 来做这件事,请输入以下内容:

$ npm i @tanstack/react-query

或者,如果你更喜欢使用 yarn,请输入以下内容:

$ yarn add @tanstack/react-query

一旦安装了库,我们需要添加一些最小化的模板代码。我们需要让我们的应用知道我们正在使用 React Query。我们需要使用一个特殊的包装器。你看到我在说什么了吗?是的!我们将使用一个提供者,如下所示:

// App.js
import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
//…
const queryClient = new QueryClient()
export default function App() {
//…
  return (
    <SafeAreaProvider>
    <QueryClientProvider client={queryClient}>
//…
       </QueryClientProvider>
    </SafeAreaProvider>
  );
}
//…

我们将首先从 React Query 导入必要的函数——QueryClientQueryClientProvider。然后,我们将创建一个新的 QueryClient 函数并将其传递给 QueryClientProvider。我们的应用现在可以使用 React Query 的功能,而不是简单的获取。

这是一个确保你的应用在模拟器或设备上正确运行的好时机。

一旦你确认安装新的依赖项没有在你的项目中造成意外的破坏,我们就可以在下一节中实现使用 React Query 的真实数据获取了。

使用 React Query 进行数据获取

正如你所知,我们需要为我们的应用获取一些不同的数据。我们将获取头像列表、用于动态界面的图片列表、用于“收藏的图片”界面的图片列表以及会话列表。我们可以自由地在任何地方添加 React Query 的获取操作。对于简单的查询,我们可以在组件中使用库提供的 useQuery 钩子。我们也可以编写自己的自定义钩子,包含更多的逻辑或条件。让我们从一个最简单的例子开始:查询服务器以检查用户是否已登录。

为了在设置导航以显示登录屏幕或否的顶层组件中使用 React Query 钩子,我们需要稍微重新组织一下我们的代码。我们不能在同一个组件的返回语句中同时使用 QueryClientProvideruseQuery 钩子。让我们将主组件的名称从 App 改为 AppWrapped,并在 App.js 文件中添加这个新的应用组件:

// App.js
export default function App() {
    return (
      <QueryClientProvider client={queryClient}>
      <AppWrapped />
    </QueryClientProvider>
  )
};

现在,让我们将主组件的名称从 App 改为 AppWrapped,并从子组件中移除 QueryClientProvider。让我提醒你,如果你在代码示例中迷路了,可以查看 GitHub 仓库:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-9

我们的 AppWrapped 组件应该准备好使用 useQuery 钩子。确保你首先按照以下方式导入它:

// App.js
import {
  useQuery,
//…
} from '@tanstack/react-query'
//…
const fetchLoginStatus = async () => {
    const response = await fetch(requestBase +
      "/loginState.json");
    return response.json();
  }
const AppWrapped = () => {
  const { data } = useQuery(['loginState'],
    fetchLoginStatus);
//…
{!data?.loggedIn ? (
    <Stack.Screen name='Login' component={Login} />
          ) : (
            <>
               <Stack.Screen
                name='Home'
//…

在你导入 useQuery 钩子之后,你需要创建一个负责从服务器获取和等待数据的函数。这个函数是 fetchLoginStatus,我们将将其传递给 useQuery 钩子。这个函数可以创建在任何你想要的文件中。一旦我们设置了获取操作,我们需要在组件中使用 useQuery 钩子。我们引入了一个解构的对象键 data,其中我们检查 loggedInStatus 的值。

对象解构

根据你使用现代 JavaScript 的频率,你可能已经注意到了解构语法,其中 const 关键字后面跟着花括号或方括号中的项。这种语法称为解构赋值,用于从数组(方括号)、对象或属性(花括号)中提取值。

const { data } = objectWithADataItemconst data = objectWithADataItem.data 是相同的。

现在我们已经看到了一个简单的例子,让我们看看稍微复杂一点的内容,创建一个自定义钩子和一个依赖查询。

获取图像数据

获取图像数据可能就像获取登录状态数据一样简单;然而,我想谈谈一些更复杂的事情。所以,我们将通过确保只有在用户登录后才能获取图像来人为地使我们的生活复杂化。我们将从在新建的 queries 文件夹内创建一个名为 useCustomImageQuery 的自定义钩子开始。我们的自定义钩子将返回一个 useQuery 钩子:

// src/queries/useCustomImageQuery
import { useQuery } from "@tanstack/react-query";
import { requestBase } from "../utils/constants";
const getImages = async () => {
  const response = await fetch(requestBase +
    "/john_doe/likedImages.json");
  return response.json();
}
export const useCustomImageQuery = () => {
  const { data } = useQuery(['loginState']);
  return useQuery(
    ["imageList"],
    getImages,
    {
    enabled: data?.loggedIn,
  });
};

我们首先导入了必要的 useQuery 函数和我们的工具函数 requestBase。接下来,我们创建了一个名为 getImages 的获取函数。这个函数从指定的 API 端点获取数据并返回它。最后,我们创建了一个自定义钩子,名为 useCustomImageQuery。在钩子的第一行,我们检查 loginState 查询。它看起来与我们在 App.js 中首次使用它的样子不同,不是吗?它只有一个参数:loginState。在这个 React Query 的世界里,这个参数被称为 查询键,它实际上是解锁 React Query 力量的钥匙。使用这个键,你可以访问任何之前获取的数据;你也可以手动使其无效或修改它。至于我们,我们现在只需要检查登录状态,使用这个特定的查询键。

我们自定义钩子的 return 语句由一个带有三个参数的 useQuery 钩子组成。首先,我们有至关重要的查询键,imageList。接下来,我们看到对获取函数的调用。最后但同样重要的是,我们有一个包含名为 enabled 的键的配置对象。这个键决定了何时调用给定的查询。在我们的例子中,当 loginStatus 查询的结果返回 true 值时,将调用查询。我们刚刚成功设置了 React Query 来获取图像。剩下要做的就是显示它们。让我们转到 ListOfFavorited 组件,我们将用以下自定义钩子替换上下文调用:

// src/components/ListOfFavorited.js
import { useCustomImageQuery } from "../queries/
  useCustomImageQuery";
//…
export const ListOfFavorites = ({ navigation }) => {
  const { data: queriedImages } = useCustomImageQuery();
//…
  return (
//…
    <FlatList
    data={ queriedImages }
//…

如果一切按计划进行,你现在应该能够运行应用程序并看到由 React Query 从后端拉取的收藏图像列表。如果你遇到任何问题,请记住,我们创建的自定义钩子只是一个函数,可以像这样进行调试。你可以在组件中、在钩子中或在钩子调用的 getImages 函数中放置 console.log

希望你能够顺利地设置好一切。在本节中,我们练习了使用 React Query 来获取和显示数据。我们利用了 ReactJS 的知识——因为我们创建了一个自定义钩子——但 React Query 钩子可以以多种方式设置。鉴于我们的应用程序有一个只能提供数据的模拟后端,这就是我们在 React Query 的实际使用中能走多远。不过,我亲爱的读者,我邀请你继续阅读,了解这个库还包含哪些其他优秀功能。

其他 React Query 功能

如上所述,我们无法在我们的示例应用中使用 React Query 在服务器上突变数据,因为我们的后端不够健壮。在实际应用中,你可能会使用一个既能接受POST请求也能接受GET请求的 API。在这些情况下,你将能够借助 React Query 来更改数据。为了做到这一点,我们得到了另一个专门的钩子:useMutation。以下是我们如果能够使用它来处理收藏图片时这个钩子的样子:

  const imageListMutation = useMutation(newImage => {
    return fetch('/john_doe/likedImages ',
      {method: 'POST', body: newImage})
  });

前面的函数非常简单。它将一个fetch调用包裹在 React Query 实用工具中。这个实用工具为我们提供了一些东西,比如它有以下状态:isIdleisLoadingisErrorisSuccess。我们可以检查这些状态并根据情况更新视图。我们将在ImageDetailsmodal中使用这个突变:

// src/surfaces/ImageDetailsmodal.js
//…
export const ImageDetailsmodal = ({ navigation }) => {
  const imageListMutation = useMutation(newImage => {
    return fetch('/john_doe/likedImages ',
      {method: 'POST', body: newImage})
  });
//…
  return (
//…
      <Pressable
          onPress={() => {
           imageListMutation.mutate({route.params.imageItem
             })
          }}
        >
        {mutation.isLoading ? (
            <Text>Loading…</Text>
              ) : (
                <Ionicons
                  //…
          /> )
          }
        </Pressable>
//…

让我重申:我们正在进行发送数据到服务器的干运行,因为我们的应用后端无法处理POST请求。

在前面的代码中,我们首先向ImageDetailsModal添加了一个 React Query 突变函数。我们将其传递给Pressable组件。然后,在Pressable组件内部,我们添加了一个三元运算符来检查突变是否处于加载状态。如果是的话,我们将显示一个Text组件,显示isSucccessisError,你可能会更优雅地处理加载。

这听起来很棒,但按照我们上面实现突变的方式,我们仍然需要传统地重新获取数据,以便在ListOfFavorites组件中获取最新版本。除非我们使用 React Query 的全部力量来更新之前通过useCustomImageQuery钩子获取的数据的缓存版本!以下是我们在突变中需要更改的内容:

const updateImges = () => {
   return fetch('/john_doe/likedImages ',
     {method: 'POST', body: newImage})
}
const imageListMutation = useMutation(updateImges, {
   onSuccess: data => {
    queryClient.setQueryData(['imageList'], data)
  }
})

在前面的代码片段中,我们首先提取了fetch函数以提高可读性。然后,我们将onSuccess逻辑添加到突变中,并告诉它使用imageList查询键更新标记的项目的新数据。多亏了这个策略,我们不必每次突变发生时都手动更新imageList数据。你可以在进一步阅读部分链接的 TanStack 文档中了解更多关于突变响应后更新的信息。

我们已经涵盖了 React Query 的两个最重要的方面:获取和突变数据。然而,在实际项目中还有很多更多功能可以利用。你可以检查获取状态,就像我们在示例突变中所做的那样。你也可以进行并行查询以同时获取数据。如果你想的话,你可以在获取完成之前设置初始数据来填充你的视图。你也可以在任何需要的时候暂停或禁用查询。对于大型数据集,有一种特殊的查询类型,即分页查询,它将数据批量处理成可消费的块。如果你的数据是无限的,React Query 提供了无限查询的实用工具。许多大型应用可能会利用页面加载时预取数据。

我鼓励您,亲爱的读者,阅读 React Query 文档,以便能够掌握它提供的所有可能的解决方案。我自己在使用 React Query 时也感到惊讶,因为这个库可以解决许多常见问题。

React Native 的 React Query 实用工具

正如我们所知,与纯 ReactJS 相比,React Native 有其独特的特性。React Query 并没有将管理这些特性的任务留给开发者,而是提供了一些有趣的解决方案。例如,有一个onlineManager可以添加到 React Native 应用中,以便当应用在线时重新连接。如果我们希望在应用聚焦时刷新或重新获取数据,我们可以使用 React Query 的focusManager与 React Native 的AppState一起使用。在某些情况下,我们可能希望在应用中特定屏幕聚焦时重新获取数据,React Query 也为此用例提供了解决方案。如果您想详细了解这些实用工具及其使用方法,请访问 TanStack 文档tanstack.com/query/v4/docs/react-native

摘要

React Query 经过实战检验,适用于扩展应用程序,并且可以成为各种项目的绝佳解决方案。在本章中,我们在 Funbook 应用中安装了它,并将其添加到应用中。由于我们的项目规模较小,不需要对默认配置进行任何更改,所以我们没有进行任何特定的配置。然后,我们探讨了如何使用简单的数据获取机制来检查用户的登录状态。接下来,我们创建并使用了一个具有依赖关系的更复杂的数据获取钩子。我们展示了获取到的数据,然后我们对其他 React Query 实用工具进行了浏览。React Query 是我们穿越 React Native 应用状态管理库世界的最后一站。我希望您喜欢这次旅程!

我邀请您,亲爱的读者,与我一起进入最后一章,我们将总结我们在 React Native 应用状态管理主题上所学的所有内容。

进一步阅读

第四部分 – 摘要

在本部分中,读者将概述本书涵盖的所有不同解决方案。

本部分包括以下章节:

  • 第十章附录

附录

好吧,我亲爱的读者,我们已经到达了这本书的最后一部分:总结。我真诚地希望你喜欢阅读我关于 React Native 中状态管理库的讨论,并且我想感谢你一路走来。现在,让我带你回顾一下这本书中我们讨论的所有内容。如果你在之后对我的想法和沉思不太感到疲倦,你将找到关于状态管理的招聘面试问题相关的附加部分。

在这本书的前几章中,我们广泛地探讨了网络开发的历史。我们看到了互联网景观的演变,这导致了ReactJS的创建。然后,我们讨论了React本身的演变,这导致了 React Native 的创建。了解 React Native 与 ReactJS 的紧密联系在开发 React Native 应用时非常有帮助。ReactJS 社区比其移动优先的表亲更大、更成熟。许多 React Native 开发者面临的问题都可以用 ReactJS 知识来解决。有一个叫做React 心态的概念,这对于编写健壮、可扩展和无 bug 的应用至关重要。关于这个主题有很多优秀的文章,例如官方 React 文档中发布的Thinking in React文章。一旦我们学会了如何采用这种心态,我们就开始构建我们自己的应用:Funbook。

毫不奇怪,我们创建的应用程序是一个社交媒体克隆应用。社交媒体应用是示例代码的一个有趣话题,因为我们都非常熟悉它们应该如何工作。同时,它们比大多数 ReactJS 教程中出现的传统待办事项应用要复杂得多。设置任何移动应用本身就是一项任务。对于所有那些网络开发者来说,在移动应用领域工作是一个全新的领域,拥有自己的工具和流程。幸运的是,我们可以利用Expo,几分钟内就能拥有一个功能齐全且可测试的应用。一旦我们对基本的应用设置感到舒适,我们就开始编写真正的 Funbook 应用。我们添加了一些界面:动态、对话、喜欢的图片和相机。然后我们开始用 React 思考!我们规划和编写了所有界面的底层组件。我们使用了许多现代 React 特性,例如 hooks 和 context。到第四章(B18396_04.xhtml#_idTextAnchor048)结束时,我们拥有了一个美丽、功能齐全的移动应用,我们可以在真实设备上或在我们电脑屏幕上的手机模拟器上对其进行测试。这看起来可能像是一项大量的工作,但让我向你保证:在 React Native 及其一些 JavaScript 前辈出现之前,创建在AndroidiOS上工作的移动应用要复杂得多!

第五章在我们的 Funbook 应用中实现 Redux,是第一个讨论 React Native 应用中状态管理外部解决方案的章节。我们讨论的具体解决方案是ReduxRedux Toolkit。截至本书写作时,Redux 是 React 社区中最古老、最广为人知且使用最广泛的状态管理库。如果使用得当,它是一个伟大的工具。它需要相当多的样板代码,而且其创造者对其实现方式有所怀疑。然而,Redux Toolkit 背后的团队在保持该库对开发者友好和更新方面取得了巨大进步。我们在 Funbook 应用中配置了 Redux 和 Redux Toolkit,并了解了如何使用它们来管理喜欢的图片列表。

在下一章中,我们讨论了被认为是 React 社区中第二受欢迎的库:MobX。到那时,我们已经掌握了 ReactJS、React Native 的扎实知识,并对如何仅使用 React 或与 Redux 一起管理全局状态有一些思考。MobX 邀请我们重新思考一些先入为主的观念,并以不同的方式看待全局状态管理。MobX 不是通过复杂的组件网络传递 props 或 actions,而是为我们提供了将全局状态数据作为任何其他 prop 使用的工具,同时只通知组件它们正在被观察。后来我们了解到这种全局状态管理有时被称为基于代理的。状态管理库位于用户和代码之间,以一种类似网络代理的方式在无形层中管理状态。MobX 有时与Valtio,另一个基于代理的状态管理库相提并论。

在了解了 MobX 的可观察性、动作以及它们推导状态值的方法(应尽可能多地进行)之后,我们准备开始使用它。我们实现了与 Redux 相同的功能——管理喜欢的图片列表。一旦在 MobX 中实现了这个功能,我们就转向下一个状态管理库:XState

Xstate 不如 Redux 和 MobX 受欢迎,但它提供了另一种看待全局状态管理的方式。而且,它还提供了一种专门的工具来做这件事!Xstate 可视化器是一个令人难以置信的工具,可以用于任何应用中的任何全局状态。在创建新应用时,能够看到不同状态片段如何相互关联可能会很有帮助。Xstate 不仅提供了这个伟大的工具,而且其创造者还邀请我们采取更数学的方法来管理状态。多亏了他,我们可以学习什么是状态机,以及应用中的每个全局状态部分都应该始终处于定义状态。

在尝试了 Xstate 并当然地使用它实现了喜欢的图片列表之后,我们准备继续前进。我们接下来查看的库是Jotai

当我开始写这本书时,Jotai 被视为新晋的佼佼者。那已经是很多个月以前的事情了!在撰写这个总结的时候,有几款新的状态管理库出现。我担心它们还不够成熟,无法与 Redux 和 MobX 这样的巨头一起分析。然而,Jotai 在过去几个月里一直保持着强劲势头,并越来越受到社区的重视。Jotai 受 useState 钩子的启发很大。最大的不同在于,Jotai 的原子将在整个应用中自由可用,无需不愉快的属性钻取或大量的样板代码。对我来说,使用 Jotai 来处理喜欢的图片列表感觉有点神奇:最少的配置,我们就可以在任何我们想要的地方访问状态片段!

一旦我们在 Funbook 应用中使用了 Jotai,我们就准备放弃它并继续前进。接下来的事情与它的前辈们非常不同——React Query,以及我们可能根本不需要任何状态管理库的观念。React Query 不是一个状态管理库;它是一个为更好地管理应用和服务器之间的数据同步而创建的库。它的目标是减少网络调用同时保持数据的相关性。在开发者体验方面,它也是一个令人难以置信的解决方案。文档详尽无遗,并配有专门的博客。成百上千的常见开发者问题都在库内部得到了解决。我们使用了 React Query,或者 TanStack Query,来获取喜欢的图片列表。不幸的是,由于 Funbook 应用的后端相当简单,我们无法使用它提供的其他功能,例如数据突变。

React Query 的创造者提出了一个非常好的问题:你真的需要为你的应用使用状态管理库吗?让我们也来问自己同样的问题。我们能够仅使用 React 就创建了 Funbook 应用。我们也曾尝试将 React Query 与本地状态混合使用。这意味着所有专门的状态管理库都应该和这本书一起从地球上抹去吗?当然不是。

选择一个状态管理库,当从经过实战检验的解决方案中进行选择时,这主要取决于开发者的经验。你的应用程序的最终用户不会知道你是否在使用 Jotai 或 Redux,但你的同行开发者可能会对此提出很多意见。一些开发者对 Redux 如痴如醉,而另一些则宁愿不接触基于 Redux 的项目。在社区中有一个无声的全球共识,即状态管理库不应该用于在应用程序中获取和持久化数据。这项任务应该留给更适合的库,例如 React Query。所以,也许你下一个创建的应用程序将使用 MobX 进行本地状态管理,React Query 进行数据获取?或者也许使用 Xstate 进行本地状态管理,Axios 进行数据获取,以及 Async Storage 进行状态持久化?或者也许是其他完全不同的东西。我相信每个状态管理库都有其优点和缺点。我也相信讨论哪个更好是一个无意义的问题,因为它们在客观上都不是更好的。我希望通过这本书,你能够“浅尝辄止”地了解几种不同的解决方案,并且更加了解你个人的偏好。一旦你找到了你喜欢的,那就享受与之一起工作的乐趣吧!

奖励内容

谈到工作:你可能会发现自己,亲爱的读者,正在参加面试,面试官会问你关于 React、React Native 和状态管理解决方案的问题。我遇到过一些问题,我认为这些问题要么非常常见,要么非常有趣。我整理了一份这些问题列表,希望它能帮助你顺利通过下一次招聘。关于 React 和 Redux 的问题在大多数与 React 和 React Native 软件开发相关的职位面试中都会出现。如果你指定你熟悉给定的库,可能会被问到关于其他状态管理库的问题。说实话,80% 的工作机会都会列出 React 和 Redux。我希望能在这几个月和几年内有所改变,因为其他状态管理库提供了很好的解决方案。以下是一些常见或有趣的问题:

  1. 在 React 中,propsstate 之间的区别是什么?

  2. 在 React Native 应用程序中,使用外部状态管理库是必要的吗?

  3. 在 Redux 中,什么是 reducer 和 action?

  4. 在 Redux 中,使用 selectors 的优势是什么?

  5. 在 Redux 中,你能否直接更改状态值?

  6. 在 MobX 中,什么是模型?

  7. 在 MobX 中,你如何使组件感知全局状态值?

  8. 在 Xstate 中,什么是状态机?

  9. 在 Xstate 中,你如何通过状态机传递额外的数据?

  10. 在 Jotai 中,最基本的州状态叫什么名字?

  11. 你能否只用 React Query 就替换所有的状态管理?

我只给你这些问题,因为给你答案可能会太简单了,你不这么认为吗?如果你必须回到书中去研究答案,或者也许简单地谷歌一下,那么信息更有可能留在你脑海中。

我衷心希望您阅读这本书的乐趣和我写作这本书的乐趣一样!感谢您一直陪伴在这里,并且随时可以通过 Twitter(如果这本书出版时它仍然存在)联系我!晚安,祝您好运!