ReactNative-状态管理简化指南-全-
ReactNative 状态管理简化指南(全)
原文:
zh.annas-archive.org/md5/fd5930eef40de8fb3cdfa93d7033a35c译者:飞龙
前言
欢迎来到 React Native 应用的奇妙世界!感谢这项技术,你可以在几分钟内拥有自己的原生应用并投入使用。如果你是第一次听说 React Native,不必担心。基本的 JavaScript 知识就足以让你迅速上手。我们将一起学习 React 和 React Native 的重要概念,并了解 React Native 生态系统、设置和工具。到第四章,为 Funbook 应用添加样式和内容,结束时你将手握一个完全功能化的社交媒体克隆应用。
这里开始变得有趣起来。我们的应用有几个 API 端点,需要在多个组件和屏幕之间管理数据对象。这种情况在中型和大型应用中非常常见。正因为如此,针对这个常见问题有许多解决方案。许多开发者使用经过实战检验且广受赞誉的开源库,如 Redux 或 MobX。其他人寻求创新的想法,并在他们的项目中选择了 XState 或 Jotai。还有一些人仍然使用内置的 React 功能,或者使用 React Query 专注于数据获取而不是状态管理。在这本书中,我们将站在所有这些类型开发者的角度。我们将从应用中挑选一个特定的功能——带有喜欢图片列表的点赞按钮——并逐一尝试这里列出的开源库。
当我们在本书结束时取得胜利,你将对在 React Native 应用中管理状态的不同方式有很好的理解。我希望你也会有一个自己的想法,以及为什么。Redux、MobX、XState、Jotai 和 React Query 都是为了解决相同的问题而创建的,但它们的创造者采取了非常不同的方法。我也希望你会像我写作这本书一样喜欢这本书。
本书面向的对象
这本书是为 React 和 React Native 世界的初学者准备的。它涵盖了与 ReactJS 软件开发相关的基本主题。即使你对基本的 React 解决方案有所了解,你也许对 MobX、XState、Jotai 或 React Query 还是新手,这意味着这本书同样适合你。
本书涵盖的内容
第一章,什么是 React 和 React Native?,将从简要回顾网络开发的历史开始,以便更好地理解 React 和 React Native 背后的理念。我们还将讨论 ReactJS 的概念,并熟悉 React Native 代码。
第二章,在简单的 React 应用中管理状态,将讨论 React 开发者面临的某些现实问题。我们将专注于中等和大型应用的稳健状态管理。由于 React 本身并没有创建用于管理全局状态的工具,我们将探讨现代 React 解决方案和其他状态管理策略。
第三章,规划和设置 Funbook 应用,将真正开始编码!我们将创建我们自己的应用,一个社交媒体克隆应用,名为 Funbook。我们将了解流行的工具,特别是 Expo,以及 React Native 生态系统。
第四章,为 Funbook 应用进行样式设计和数据填充,将专注于使我们在手中的应用看起来更好。我们还将填充一些数据,以便我们可以工作在一个接近真实生产应用的项目上。
第五章,在我们的 Funbook 应用中实现 Redux,将探讨 Redux 动荡的历史,然后介绍如何在应用中配置 Redux 和 Redux Toolkit。一旦设置好依赖项,我们将继续使用 Redux 来实现喜欢按钮和喜欢图片列表。本章包括我与主要 Redux 和 Redux Toolkit 维护者——Mark Erikson(也以 Twitter 昵称@acemarke知名)的简短对话回复。
第六章,在 React Native 应用中使用 MobX 作为状态管理器,将回到我们在第一章到第四章中创建的裸 React Native 应用,这次,我们将添加 MobX 和 MobX-State-Tree。我们将从了解这个库是如何产生的开始,然后继续在 Funbook 应用中配置它。一旦我们准备好了,我们将用它来处理喜欢图片列表和喜欢按钮。本章包括我与 MobX-State-Tree 维护者——Jamon Holmgren 的交流回复。
第七章,使用 XState 在 React Native 应用中解开复杂流程,将深入探讨一些高级数学问题,因为 XState 基于高级数学概念。当我们掌握了这些概念后,我们将继续在 Funbook 应用中配置 XState,并用于喜欢图片功能。
本章包括我从 XState 的创建者——David Khourshid(在互联网上更知名为 DavidKPiano)那里收到的回复。
第八章,在 React Native 应用中集成 Jotai,将再次回到裸 Funbook 应用,这次,我们将实现本书中最新状态管理库:Jotai。我们将了解其概念,配置它,并用于喜欢按钮和喜欢图片列表功能。本章包括我与 Jotai 创建者——Daishi Kato 的对话回复。
第九章,使用 React Query 进行服务器端驱动状态管理,将以全新的方式探讨状态管理问题:也许我们根本不需要状态管理库。也许我们唯一需要做的就是有效地管理数据获取。为了测试这个假设,我们将安装、配置和使用 React Query,也称为 TanStack Query。
第十章,附录,将全面回顾本书中我们所学到的所有内容。我还包括了一些与 React Native 应用状态管理主题相关的常见面试问题。
为了充分利用本书
您需要在您的计算机上安装 Expo。所有代码示例都已使用 macOS 上的 Expo 44 进行测试,但它们应该与未来的版本发布兼容。
| 本书中涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Expo 44 | Windows、macOS 或 Linux |
| JavaScript (ECMAScript 2020) | Windows、macOS 或 Linux |
| ReactJS v18 及以上 | Windows、macOS 或 Linux |
| React Native | Windows、macOS 或 Linux |
在第三章,规划和设置 Funbook 应用中详细介绍了额外的设置说明。
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将有助于避免与代码的复制和粘贴相关的任何潜在错误。
本书代表了 2022 年最知名的状态管理库的状态。我鼓励您尝试一些新的、不太为人所知的解决方案,因为每天都有新的库发布。
下载示例代码文件
您可以从 GitHub 在github.com/PacktPublishing/Simplifying-State-Management-in-React-Native下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。
我们还提供了来自我们丰富的书籍和视频目录中的其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表的彩色 PDF 文件。您可以从这里下载:packt.link/wv4Mk。
使用的约定
本书中使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“export const ListOfAvatars = () => {”。
代码块设置如下:
Import { Text } from 'react-native';
const Welcome = () => {
return <Text>Hello, World! </Text>;
}
当我们希望将您的注意力引向代码块的一个特定部分时,相关的行或项目将以粗体显示:
return (
<View style={{ paddingTop: 30 }}>
<FlatList
data={arrayOfAvatars}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
</View>
任何命令行输入或输出都按照以下方式编写:
$ yarn add react-query
$ expo start
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“如果您想在手机上查看您的应用,您可以在“Expo Go 应用”中找到扫描的二维码,就在这里。”
浏览示例数据
您可以在任何时候查看应用中使用的示例数据。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过customercare@packtpub.com给我们发邮件,并在邮件主题中提及书名。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能向我们提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了Simplifying State Management in React Native,我们非常乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢在移动中阅读,但又无法随身携带您的印刷书籍吗?
您的电子书购买是否与您选择的设备不兼容?
请放心,现在购买每一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何时间、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不仅限于此,您还可以获得独家折扣、时事通讯和每日免费内容的访问权限。
按照以下简单步骤获取好处:
- 扫描下面的二维码或访问以下链接

packt.link/free-ebook/978-1-80323-503-5
-
提交您的购买证明
-
就这些!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件。
第一部分 – 学习基础知识:React、状态、属性、钩子和上下文简介
在这部分,我们将从一些对创建真实应用至关重要的理论知识开始。读者将了解 React 的一些历史以及其一般规则、指南和最佳实践。接下来,我们将探讨 React 处理状态的内置策略:本地状态、props、hooks 和 context。
本部分包括以下章节:
-
第一章, 什么是 React 和 React Native?
-
第二章, 在简单的 React 应用中管理状态
第一章:什么是 React 和 React Native?
欢迎来到React和React Native的神奇世界。希望您在这里能感到宾至如归。如果您是第一次接触这些框架,或者您已经稍微尝试过它们,那都没关系。本书将专注于管理 React Native 应用的状态,但我们将从基础知识开始讲起。
如果 React 和 React Native 是人的话,第一个将是第二个的父亲。您可以专注于子框架,但您会发现了解 React Native 的“父亲”——ReactJS有很大的好处。
我们将开始这段旅程,先回顾 ReactJS 的历史,特别是它为什么被创造出来。然后,我们将继续研究 ReactJS,探讨React 思维模式或拥有React 心态的含义。熟悉 ReactJS 后,我们将尝试理解跨平台软件开发的含义以及 React Native 在跨平台开发生态系统中的位置。为了理解这个生态系统,我们将专注于 React Native 本身,它的简要历史和当前状态。我们将通过一些用 React Native 编写的原生应用示例结束我们的旅程。
在本章中,我们将涵盖以下主题:
-
理解 ReactJS 的历史
-
React 思维模式(React 心态)
-
理解跨平台软件开发
-
了解 React Native 的历史
-
查看使用 React Native 的流行应用的示例
到本章结束时,您将具备 React 和 React Native 的高级知识。您还将了解它们在软件开发生态系统中的位置。
理解 ReactJS 的历史
在本节中,我们将简要回顾 ReactJS 的历史。如果您对这个特定主题不感兴趣,请随意跳过本节,直接进入React 思维模式。了解一个框架的历史对于使用它并不是强制性的。如果您更喜欢 YouTube 上的浓缩知识,我强烈推荐观看由uidotdev发布的 10 分钟视频,名为React 的故事。
前驱者
你知道吗,第一个创建的网站至今仍然存在?你可以在这里找到它:info.cern.ch/hypertext/WWW/TheProject.html。它是在 1991 年创建的!从那时起,发生了许多变化。首先,网页开发者们想要改变他们网站的样式,因此 CSS 被创造出来。几年后,同样的开发者们希望在他们的美丽网站上看到更多的互动性。这就是JavaScript在互联网上找到其位置的时候。但正如我们所知,网络永远不会停止发展。JavaScript 的过度使用导致了像 jQuery、BackboneJS 和 Ember 这样的库的创建。每个库的创造者都从他们的竞争对手那里吸取了教训。他们做出了导致创建非常不同的开发者体验的决定。开发者们有自己的偏好,并且就哪个库更好展开了小规模的战争。
对于这个问题没有正确答案。然而,可以肯定的是,无论幕后使用的是哪个库,网站的用户体验都发生了演变。网站变得更加互动,能够适应用户的屏幕大小。例如,今天为移动视图和桌面视图创建单独菜单是一种常见的做法。这可以通过 JavaScript 或 CSS 单独实现。这种用户体验的转变没有 JavaScript开源库的演变是不可能发生的。
几年后,随着越来越多的 JavaScript 代码片段被添加到网站上,是时候需要一个更全面的解决方案了。第一个突破来自谷歌,推出了AngularJS。AngularJS 于 2010 年正式发布,与当时市场上的其他解决方案不同。这不仅仅是一个库;这是一个框架。开发者们能够快速创建复杂的交互,他们不再害怕对 JavaScript 文件的任何更改都会破坏整个页面。我不想深入探讨 AngularJS 的实现细节。毕竟,这不是本书的重点。总的来说,AngularJS 引入了后台运行的框架观察到的特殊 HTML 属性。正如你可能想象的那样,当 JavaScript 观察数十个甚至数百个元素和事件时,它会减慢速度。因此,用户体验正在受到影响,世界正准备迎接另一场 JavaScript 革命。谷歌认为他们的 Angular 版本 2 将保持领先地位,但到了 2013 年,Facebook 的开发者宣布了 ReactJS 的发布。
然后出现了 React
ReactJS 被特别定位为一个用户界面(UI)库。它被构思用于网站上的最终用户交互。它还使用了JSX——为 React 创建的 JavaScript 扩展。许多开发者对这个新语法表示愤怒,这并不令人意外。不过,我要说的是,在科技界,愤怒的反应并不意外。任何新的技术解决方案都必须经受住愤怒的 Reddit 帖子,这些帖子说它丑陋、无用,或者简单地说,很糟糕。幸运的是,ReactJS 的开发者并没有因为这种最初的负面反应而停止工作。此外,了解 ReactJS 的开发者成为了它的倡导者。你可能会问,为什么 ReactJS 能够经受住时间的考验,而 Angular 却没有?我认为这与框架的高级思维模式有关。ReactJS 提出了优雅、简单的解决方案,同时完全可配置以满足任何需求。我将在下一节进一步探讨这种思维模式。
回到我们的历史课程!我们回到了 2013 年,ReactJS 以一种震撼的方式进入舞台。许多人讨厌它,但其他人用它来构建越来越复杂的网站。不幸的是,ReactJS 的扩展性并不好。你的 React 组件使用状态和属性。如果父组件创建了一个状态,这个状态需要被层级较低的四个或五个组件读取,你将遇到被称作属性钻取的问题。属性钻取意味着开发者必须通过许多父组件传递必要的属性,以便到达最终需要读取它的子组件。这个过程既令人烦恼又无聊!这就是第一个状态管理库——Redux诞生的时刻。我们将在下一章详细讨论 Redux 和其他状态管理库。
在撰写这本书的时候,ReactJS 是最受欢迎的 JavaScript 库之一。它不断进化,其维护者对公众讨论和建议持开放态度。2019 年,他们引入了 hooks 和 context。这两个 React 实用工具可以满足你大部分的状态管理需求。它们之所以被创建,是因为 React 团队意识到使用 React 的开发者需要在状态管理方面得到改进。
在 hooks 和 context 引入之前几年,具体是在 2015 年,Facebook 开发者发布了 React Native。这本书的真正英雄!但让我们不要走得太远。在这个时候,了解 React 的基本概念非常重要。让我们继续探讨 React 思维模式。
React 思维(React 思维模式)
官方 ReactJS 文档中包含一个名为 React 思维 的章节:https://reactjs.org/docs/getting-started.html#thinking-in-react.
重要提示
许多 React 用户认为阅读React 思维(reactjs.org/docs/thinking-in-react.html)是他们理解 React 的关键时刻。这可能是最古老的 React 教程,但它仍然同样相关。
让我们尝试捕捉那篇文章中最重要且仍然相关的部分。
首先,当我们用 ReactJS 创建一个网站时,我们需要考虑我们将如何构建我们的组件。不是 HTML 块,不是 DOM 元素,而是组件。理想情况下,每个组件都将是一个独立的实体,它要么创建状态,要么消费属性,有时两者都有。组件是我们应用程序的最小部分,就像原子是我们世界的最小部分一样。
好吧,我意识到原子可以进一步分为中子、质子和电子。ReactJS 组件也可以分为处理逻辑和实际渲染的部分。然而,原子和 ReactJS 组件都是它们各自领域的基本构建块。
现在我们已经想象出了我们的组件,我们需要知道它们应该如何相互交互。让我们回到 ReactJS 文档,在那里我们将找到一个很好的章节,组合与继承:reactjs.org/docs/composition-vs-inheritance.html。
这篇文章非常明确地指出,ReactJS 组件应该是组合的,而不是严格分层堆叠的。这基本上意味着任何子组件都应该以可以被应用程序中的其他父组件重用的方式创建。这促进了原子组件的高可重用性,同时,减少了创建应用程序所需的代码量。
现在我们已经掌握了理论,让我们继续探讨具体实践。我们如何在实践中组合 ReactJS 组件?通过使用状态和属性。你可能想知道那些是什么?好吧,我很乐意解释!
状态和属性(简称属性)都是普通的 JavaScript 对象。它们之间最大的区别在于属性是只读的,而状态可以在管理它的组件内部进行更改。状态是真相的来源,而属性是应用程序当前状态的表示。让我们看看一个最小化的代码示例:
import React, { useState } from "react";
const PrettyButton = ({ updateCount, count }) => {
return (
<button onClick={updateCount}>This was clicked {count} of times</button>
);
};
export default function App() {
const [counter, updateCount] = useState(0);
const handleClick = () => {
updateCount(counter + 1);
};
return (
<div>
<h1>Hello There!</h1>
<PrettyButton count={counter} updateCount={handleClick} />
</div>
);
}
你可以通过这个 CodeSandbox 在线尝试这段示例代码:codesandbox.io/s/admiring-fire-68k94x?file=/src/App.js。
从前面的代码示例中,你可以看到App组件创建了计数器状态,以及负责更新它的函数。PrettyButton以属性的形式消费这个状态。PrettyButton不能直接更改counter或updateCounter的值。
如果我们要编写另一个需要使用PrettyButton的父组件,它将需要创建自己的counter和updateCounter状态。正因为如此,我们可能想要在我们的 web 应用中使用PrettyButton的每一个实例都将独立于其他实例。
我们还可能在主App组件中导入多个子组件。这是完全自然的。我们可能有一个带有按钮、文本和模态框的应用,所有这些都需要显示按钮被点击的次数。我们只需要将必要的组件添加到父组件中,并传递counter属性。状态仅在父组件中更改,然后传递给子组件。
现在,我们来到了需要决定哪个组件应该处理状态变化的时候。在我们的简单代码示例中,答案很明显:我们只有一个父组件。在现实世界中,这个问题可能要难得多。幸运的是,我们将在整本书中探讨状态管理策略。我希望,在阅读这本书之后,你将准备好在你的 React Native 应用中选择最佳位置来存储和管理你的应用状态。
在上一节中,我们讨论了在 ReactJS 中编写代码的高级方面。记住我们查看的模式是很有用的,因为它们在 React Native 开发中同样有用。而且由于我们已经熟悉了 ReactJS,我们准备好深入到用 JavaScript 编写的原生应用的世界。
理解跨平台软件开发
在谈论 React Native 之前,我们需要回顾一下移动应用开发的格局。
显然,可以使用原生平台编程语言来创建移动应用。被认为最现代的是用于 iOS 开发的 Swift 和用于 Android 开发的 Kotlin。许多开发者仍然使用 Objective-C 和 Java。然而,当手机市场在苹果和谷歌两大巨头之间稳定下来时,创建一次编写即可用于两个平台的解决方案是非常诱人的。同样,对于可以在任何浏览器中打开的网站,为什么我们不能有可以在任何设备上运行的应用呢?
寻找这种神话般的跨平台解决方案对许多公司来说极具吸引力。他们分别从 iOS 和 Android 团队中招聘了不同的团队,最终得到的 app 在外观和感觉上并不相同。
软件开发的世界非常广阔,我们可以找到许多针对单个问题的解决方案。跨平台开发也不例外。如果你搜索“跨平台应用”,你会找到一个由微软提供的解决方案,名为Xamarin。你还会找到一个名为Flutter的解决方案,它使用一种名为Dart的语言编写。最后,你还会发现许多基于 JavaScript 的解决方案。其中第一个有意义的参与者是Ionic。Ionic 是一个框架,建于 2013 年,用于 AngularJS 的开发,并在幕后使用Apache Cordova。Ionic 开发者使用与创建网站相同的语法来构建他们的应用。在构建时,创建一个包含单个 WebView 的原生应用包装器。Ionic 代码在这个 WebView 中运行。鉴于这种结构,许多人将 Ionic 应用称为混合应用,以区分它们与跨平台应用。
React Native 是一个完全不同的解决方案。在这种情况下,代码被编译成一个完整的原生应用。JavaScript 代码在应用中运行,并通过一个桥接器与手机的本地模块进行通信。但你可能会问,React Native 是从哪里来的呢?
让我们在下一节深入探讨这个话题。
探索 React Native 的历史
回到 2012 年,Facebook 宣布他们正在成为一个移动优先的公司。Facebook 意识到用户在手机上花费的时间比在电脑上多。他们需要确保他们的网站和应用在智能设备上无缝工作。然而,Facebook 的大多数工程师都是网页开发者。公司开始研究如何利用这些网页开发者的知识来开发移动应用。在尝试了几种不同的想法后,他们不想跟随 Ionic 的脚步,将应用封装在 WebView 中。他们需要一些新的东西。
就在这个时候,一位名叫克里斯托弗·切德欧的开发者在软件开发的历史上留下了自己的印记。他与乔丹·沃尔克、阿什温·布拉姆贝和林·何一起参加了一个 Facebook 内部的黑客马拉松。基于乔丹最初的努力——此时他已经能够从 JavaScript 生成 iOS 的UILabel——他们创建了一个工作原型,可以在用户设备上从 JavaScript 生成原生 UI 元素。而且他们只用了 2 天时间!
React Native 的历史:Facebook 的开源应用开发框架
你可以在这里阅读文章:www.techaheadcorp.com/blog/history-of-react-native/。
在这次初步成功之后,乔丹和克里斯托弗能够继续他们名为 React Native 的新产品开发工作,这个产品拥有一个由工程师组成的完整团队。
经过 3 年的努力,他们终于准备好向世界展示他们的成果。React Native 的官方发布是在 2015 年的 ReactJS Conf 上进行的。这是第一届 ReactJS Conf,React Native 在主题演讲中被介绍!这显示了 Facebook 对这个框架的信心。我鼓励你查看这个演讲;你可以在官方 ReactJS 文档中找到链接:reactjs.org/blog/2015/02/18/react-conf-roundup-2015.html。
自 2015 年以来,React Native 经历了很大的成长和变化。一些变化,如 hooks 和 context 的引入,是 ReactJS 中发生变化的简单后续。在其他情况下,变化是由社区推动或由框架的维护者提出的。React Native 在github.com上有一个名为讨论和提案(github.com/react-native-community/discussions-and-proposals)的整个部分。每个人都可以添加他们想讨论的关于 React Native 实现、生态系统等主题的内容。这个论坛是了解当前正在进行的事情以及未来可能发生的事情的一个极好的资源。这个论坛上的第一个问题,确切地说,是第六个问题,是一个关于精益核心的提案。到那时,React Native 已经在野外至少 3 年了,并且已经成长了很多。这个框架已经包括了 UI 细节的实现,如开关,或原生功能,如推送通知。该仓库的核心维护者之一提议,所有非绝对必要的代码都应该从主包中移除。你可以在精益核心这里了解更多细节:github.com/react-native-community/discussions-and-proposals/issues/6。
当然,回答“什么是必要的”和“什么不是”的问题并不容易。精益核心经过了几个月的讨论和重大变更。今天主 React Native 包的形状代表了这一努力的成果。
同时,精益核心计划激发了社区的热情,让他们继续创建自己的库,这些库对 React Native 应用可能很有用。截至撰写本书时,当你决定创建一个 React Native 应用时,可以选择的库有数百个。这里有 UI 库、导航库、异步存储管理库等等。这既是福也是祸,因为并非每个库都写得很好且维护得当。不幸的是,你可能会用到一些可能在将来破坏你应用的库。所以,在你跑到终端并输入yarn add之前,你可能想使用 React Native 目录:reactnative.directory。这个网站提供了开源库的指标,当你想为你的项目添加一个好的依赖项时,这些指标非常有帮助。
有几个库非常突出,它们被认为是 React Native 项目的推荐库。这些库通常非常成熟且维护良好。一个例子是React Navigation,这是需要多于一个屏幕的应用的首选库。React Native Testing Library是与 Kent C. Dodd 的React Testing Library官方结合的库。Reanimated是一个动画库,其性能优于任何竞争对手。
React Native 生态系统的一个重要部分是Expo:expo.dev/. Expo 既是 React Native 应用的框架,也是平台。它为用户提供了一套用于开发、构建和部署应用的工具。
这具体意味着什么?Expo 是 React Native 之上的一层薄层,旨在让开发者的生活更轻松。如果你在 React Native 中编写应用就像用手吃烤牛排一样,那么 Expo 就像是带着烤土豆和凯撒沙拉一起吃菲力牛排。在一家高档餐厅。你可能非常偏好前者,但无法否认后者明显的优势。如果你决定使用 Expo,你将在官方 React Native 文档中找到本地环境设置说明:reactnative.dev/docs/environment-setup。一旦应用设置完成,你将能够利用 Expo 团队创建和维护的许多组件。这样,你可能会节省一些头痛和性能问题。当你准备好向世界展示你的应用时,你可以将你的应用包上传到 Expo 网站,并用于测试和部署。正如你所见,Expo 是一个非常通用的工具。
既然我们已经了解了 React Native 的历史和当前状态,让我们继续看看一些实际使用它的应用。
检查使用 React Native 的流行应用的示例
现在我们对 React Native 有了一些了解,是时候对其感到兴奋了。了解一项新技术的一个好方法就是看看这项技术已经被用于什么。当你必须决定使用特定技术时,这也是一个好的策略。
显而易见的例子来自Meta——React Native 的诞生地。ReactJS 的最初实现发生在 Facebook Ads 中。React Native 在移动设备上用于相同的功能是合适的。Facebook 的移动应用并非完全使用 React Native 创建,但其中一些部分使用了它。这意味着 Facebook 应用是一个 React Native 的棕色地带应用。与之相反的是仅使用 React Native 编写的应用,这类应用被称为绿色地带。
当我们在元宇宙中时,我会提到 Instagram 应用使用 React Native,Oculus 应用也是如此。
别担心,Meta 并不是唯一一家使用 React Native 的知名公司。Discord 不仅使用 React Native 来开发他们的应用,还撰写了关于他们如何维护应用的博客文章。在这篇 Medium 文章中,blog.discord.com/how-discord-achieves-native-ios-performance-with-react-native-390c84dcd502,Discord 团队表示,他们一开源就采用了 React Native,并且几年后仍然对他们的决定感到满意。
Shopify 是 React Native 生态系统中的另一大重要玩家。他们在博客上有一篇文章,标题为《React Native 是 Shopify 移动未来的未来》:shopify.engineering/react-native-future-mobile-shopify。Shopify 工程师还撰写了更多技术文章,例如关于无障碍的:www.shopify.com/partners/blog/react-native-accessibility。
网站构建巨头Wix也在 React Native 领域非常活跃。他们也写了一些关于他们与 React Native 冒险经历的文章(https://medium.com/wix-engineering/react-native-at-wix-the-architecture-db6361764da6),但他们还创建了开源库,例如这个 UI 库:github.com/wix/react-native-ui-lib.
回到使用 React Native 构建的具体应用列表,我必须提到Coinbase。以可靠的方式管理用户的财务是这家加密市场领先者的首要任务。他们分析了、迭代了,并最终选择了 React Native 作为他们的主要移动技术。你可以在他们的博客上阅读他们关于从原生技术过渡到 React Native 的文章:blog.coinbase.com/announcing-coinbases-successful-transition-to-react-native-af4c591df971。
你可能听说过像特斯拉、沃尔玛、Salesforce、彭博社和《Vogue》这样的公司。你可能也使用过像 Uber Eats、Artsy、Words with Friends 和 SoundCloud Pulse 这样的应用。它们有什么共同点?惊喜!(其实不是。)它们都使用了 React Native。你可以在 React Native 展示区找到更多例子和文章链接:reactnative.dev/showcase。
并非所有 React Native 的故事都是成功的。有一个著名的案例(我所说的“著名”是指它在推特上讨论了好几天)是 Airbnb。Airbnb 的网站使用 ReactJS,因此他们尝试为他们的移动应用使用 React Native 是合乎逻辑的。经过几年的开发,他们遇到了开发瓶颈和性能问题。他们的应用包含一个非常大的地图,需要完美运行。负责该应用的开发者经常需要 React Native 开发者的帮助,这对这家以网络技术为重点的公司来说是一个瓶颈。他们在 2018 年宣布与 React Native 分手:medium.com/airbnb-engineering/sunsetting-react-native-1868ba28e30a。幸运的是,他们仍然在开发他们惊人的动画库 Lottie(http://airbnb.io/lottie/#/),它可以在 React Native 应用中使用。
摘要
哎呦!对于一本编程书来说,理论内容确实挺多的,对吧?然而,即使你觉得有点枯燥,我坚信这些理论知识对于下一章将非常有用。我们已经了解了一些关于网络开发的历史,以及 ReactJS 和 React Native 的创造者的动机。了解这些将使我们能够理解不同状态管理解决方案背后的理念。在下一章中,我们将探讨在 React Native 应用中管理状态的最基本方式:使用 hooks 和 context。
第二章:在简单的 React 应用程序中管理状态
在上一章中,我们简要回顾了网络开发、JavaScript、ReactJS 和 React Native 的历史。尽管历史知识不是编写出色代码的必要条件,但我发现它很有用。一旦我们了解到为什么特定的库创建者鼓励某些模式并阻止其他模式,我们就可以编写出更少错误和更高效的代码。啊,是的!编写代码!这就是你在这里的原因,亲爱的读者,不是吗?好消息是,在本章中,我们将深入研究代码示例。我们将从查看 React 中最基本的数据和状态管理策略开始:使用状态和属性。然后,我们将深入比较有状态和无状态组件。一旦我们对 React 应用程序中状态的工作方式有了很好的理解,我们将继续讨论 hooks。我们将通过完成我们自己的小应用程序的设置和配置来结束本章。
这里是我们将要涵盖的要点:
-
状态是什么?它与属性有何不同?
-
有状态和无状态组件是什么?
-
什么是 hooks?为什么使用它们?
-
设置示例应用程序
到本章结束时,你应该对 React 代码感到舒适。我们还将为我们的应用程序设置基础。尽管应用程序可能彼此非常不同,但这个基本设置对于大多数应用程序来说都将保持不变。请随意将其用于你想要工作的任何其他项目。
技术要求
如果你熟悉 ReactJS 但尚未使用过 React Native,你将能够毫无问题地跟随本节内容。
如果你从未阅读或编写过任何 ReactJS 或 React Native 代码,学习基本概念非常重要。请访问官方 React Native 文档reactnative.dev/docs/intro-react,并熟悉如 组件、JSX、状态 和 属性 等关键概念。
本章的最低要求是了解 Git、基本的 命令行界面(CLIs)知识,以及 JavaScript 的实际应用。
状态是什么?它与属性有何不同?
每个 React Native 应用程序都是为了显示某种类型的数据而创建的。这可能包括天气数据、图片、市场数据、地图……使用 React Native,我们管理这些数据如何在用户屏幕上显示。React Native 提供了强大的工具来样式化和动画化内容。然而,在这本书中,我们专注于你的应用程序中使用的数据的原始材料。
为了有一个动态的数据片段,它能够自动与我们的组件保持同步,我们需要将列表声明为组件状态。
重要提示
关于状态,需要记住的最重要的事情是:状态是在组件内部管理的;它是组件的 内存。
状态的任何变化都会导致你的组件及其所有子组件重新渲染。这是一个预期的行为:如果你的数据发生变化,你希望你的 UI 也相应地改变。然而,多个组件的重新渲染可能会导致你的应用遇到性能问题。
让我们通过一个例子来更好地理解状态。我们将从一个非常基本的组件开始,包含一个<Text>元素和一个<Pressable>元素。<Pressable>是 React Native 应用中推荐使用的组件,在 Web 开发者会使用<button>标签的地方:
import React from "react";
import { View, Text, Pressable } from "react-native";
export const ManagedButton = () => {
return (
<View>
<Text>this text will display the current status</Text>
<Pressable onPress="">
<Text>Press here to check/uncheck</Text>
</Pressable>
</View>
);
};
你可能已经观察到,亲爱的读者,当点击<Pressable>组件时不会发生任何事,因为我们还没有提供onPress函数。
现在,我们将向这个简单的组件添加状态。我们将在<Text>组件内设置一个选中/未选中的文本,并将其与组件状态相关联:
import React, { useState } from "react";
import { View, Text, Pressable } from "react-native";
export const ManagedButton = () => {
const [checkedState, setCHeckedState] = useState("unchecked");
return (
<View>
<Text>this text will display the current status, which is: {checkedState}</Text>
<Pressable onPress="">
<Text>Press here to check/uncheck</Text>
</Pressable>
</View>
);
};
测试 React Native 代码比测试在浏览器中运行的代码(如 JavaScript 或 ReactJS)要复杂一些。幸运的是,Expo 的好人们创建了一个在线工具来测试代码片段。它被称为 Expo Snack,你可以用它来测试前面的代码,网址是snack.expo.dev/@p-syche/simplifying-state-management---chapter-2-example-1。
让我们逐个查看这些变化。我们首先在第一行添加从 React 库中导入useState钩子的语句。然后,在组件内部,我们设置这个变量:
const [checkedState, setCheckedState] = useState(“unchecked”);
useState钩子接受一个数组,其中第一个元素是状态值,第二个元素是设置值的函数。如果你在组件中不会改变状态,你可以省略第二个参数。关于数组中元素的名称没有官方规则,但将设置函数命名为与状态值类似,但使用"set"关键字是一种公认的习惯。最后但同样重要的是,将"unchecked"字符串传递给useState钩子。这是useState钩子的默认值。如果你不希望设置默认状态,你可以将括号留空。
现在我们已经导入了状态钩子,并使用useState钩子设置了组件状态,我们可以在组件中使用它。因此,这一行:
<Text>this text will display the current status, which is: {checkedState}</Text>
包围状态的括号是JSX的一部分。JSX是JavaScript的语法扩展,它是编写所有React组件所使用的语法。"这对你来说意味着什么?"你,亲爱的读者,问。这意味着在编写JSX时,你可以编写任何JavaScript代码,并且还可以编写额外的内容,例如用括号包裹的组件状态。你可以将JSX与JavaScript相比,就像海盗说话与普通英语相比一样。所有说英语的海盗都会理解所有的英语短语,但一个普通的英语人可能不会理解所有的海盗短语。好吧,伙计?那么我们就继续吧,嘿嘿嘿!
我们已经设置了状态,但我们的<Pressable>组件仍然没有任何作用,对吧?让我们添加一个onPress函数,它将设置状态。实现这一点最简单的方法是将setCheckedState函数从useState钩子直接传递到onPress函数中:
<Pressable onPress={setCheckedState("checked")}>
现在,当按下<Pressable>按钮时,它将改变组件的状态,这反过来又会改变<Text>组件中显示的文本。
你可以用useState钩子实现更多的事情。你可以将其设置为任何你喜欢的值,包括一个对象。每个组件都可以有多个状态,实际上可以有很多!如果你想看看如何在 React 组件中实现状态的其它示例,我邀请你查看进一步阅读部分的第一个链接。
让我们继续到本节第二个英雄:props。Props 是属性的简称。Props 就像 state 一样是 JavaScript 对象;它们之间最大的区别是 props 是只读的。
重要提示
关于 props,最重要的是记住这一点:props 是不可变的(或只读的)。
"checked"/"unchecked"状态的天然流动。父组件有子组件:带有图像或文本的组件等,我们将状态以 prop 的形式传递给它们。在这种情况下,子组件可以是"checked"或"unchecked"。但是,子组件永远不会改变文本的状态。文本的状态只能在声明状态的父组件内部改变。让我们更新我们的代码示例,包括一个父组件和一个子组件,状态在父组件中设置并通过 props 传递给子组件:
import React, { useState } from "react";
import { View, Text, Pressable } from "react-native";
const ManagedText = ({checkedState}) => {
return (
<Text>this text will display the current status, which is: {checkedState}</Text>
);
};
export const ParentComponent = () => {
const [checkedState, setCheckedState] = useState("unchecked");
return (
<View>
<ManagedText checkedState={checkedState} />
<Pressable onPress={() => setCheckedState("checked")}>
<Text>Press here to check/uncheck</Text>
</Pressable>
</View>
);
};
你可以在以下 Expo Snack 中找到前面的代码:snack.expo.dev/@p-syche/simplifying-state-management---chapter-2-example-2。
让我们从与上一个示例相同的地方开始。我们有我们的<ParentComponent>,之前它被命名为<ManagedButton>。但是说句实话,这个组件与上一个版本相比变化不大。这里唯一的改变是,我们看到了一个<ManagedText>组件,而不是一个<Text>组件,它有一个神秘的checkedState属性。这个属性被传递给<ManagedText>组件,然后传递给它内部的<Text>组件。按下<Pressable>组件将改变<ParentComponent>的状态,这也会反映在子组件<ManagedText>中。我相信,亲爱的读者,父/子命名法是非常容易理解的,不需要额外的解释。至于checkedState属性,或者简称为 prop,你应该知道你可以给它取任何你喜欢的名字;没有必要将 prop 的名字设置为与它的值相同。例如,你可以这样写:
const ManagedText = (fancyComponentStuff) => {
return (
<Text>this text will display the current status, which is:{fancyComponentStuff}</Text>
);
};
export const ParentComponent = () => {
const [checkedState, setCheckedState] = useState("unchecked");
return (
<View>
<ManagedText fancyComponentStuff={checkedState} />
<Pressable onPress={setCheckedState("checked")}>
<Text>Press here to check/uncheck</Text>
</Pressable>
</View>
);
};
如果你想了解更多关于 props 和状态的信息,可以查看官方 React 团队推荐的文章。它们列在进一步阅读部分。
现在你已经知道了状态和 props 是什么,以及它们彼此之间的不同,在接下来的部分,我们将探讨有状态组件和无状态组件。
什么是有状态组件和无状态组件?
无论你是完全新接触 React 世界,还是在这里已经有一段时间了,你很可能已经听说过有状态和无状态组件这两个术语。在 ReactJS v16.8 引入 hooks 之前,这些术语特别有用。现在不用担心 hooks——我们将在本章末尾讨论它们。
从一个高层次的角度来看,ReactJS和React Native组件不过是JavaScript函数。React 库为这些函数添加了一些特定的功能。其中之一就是状态,这是一种我们在上一节中探讨的特殊类型的组件内存。
一个可以接受状态的 React 组件可能看起来像这样:
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {name: "World"}
};
render() {
return <Text>Hello, {this.state.name}</Text>;
}
}
这种类型的组件也通常被称为“类组件”,因为它需要以这种方式声明。类组件,或称有状态组件,在componentDidMount()、componentWillUnmount()、shouldComponentUpdate()以及一些其他函数出现之前是一等公民。这些函数对于许多面临边缘情况的开发者来说是一大救星。例如,他们需要在组件的其他部分加载一些数据之前,或者在他们确保在组件卸载前清理一些副作用函数之前。不幸的是,这也意味着他们的组件在逻辑上变得越来越复杂。试图理解包含多个“生命周期方法”的文件的代码流程是一项真正的挑战。如果你想了解更多关于生命周期方法的信息,请查看进一步阅读部分,那里你可以找到一个链接到 ReactJS 文档中一篇题为在类中添加生命周期方法的文章。
有状态组件比无状态组件更难测试,而且它们编译速度较慢,编译后体积也更大。
无状态组件,也称为函数组件,是类组件的轻量级兄弟。以下是一个无状态组件的例子:
const Welcome = (props) => {
return <Text>Hello, World! </Text>;
}
比较前面片段中显示的两个示例组件,你应该注意到编写给定组件所需的代码行数有很大的差异。我们简单的有状态组件需要九行代码,而函数组件在三个代码行内就实现了相同的功能!
这意味着constructor或特殊的componentDidUpdate。当然,它们有一个很大的缺点,就是无法管理状态。所以,一个理想的ReactJS或React Native应用至少应该包含一个父组件,一个有状态的组件,然后它会将属性传递给各种无状态的子组件。然而,在现实世界中几乎没有理想的应用。开发者经常会编写有状态的组件,并添加生命周期方法来管理何时以及何时不应更新 UI。
这种趋势随着之前提到的ReactJS v16.8 的发布而改变,当时在ReactJS世界中引入了钩子的概念,我们将在下一节中探讨。
钩子是什么?为什么使用它们?
正如我之前提到的,无状态组件通常更容易编写和测试。当useState是一个返回状态值和更新它的函数时,它们应该是useState的默认组件。你可能在我们之前关于 React 组件状态的章节中见过它。
让我们回到我们之前的有状态组件的例子,将其改为函数式组件,并添加useState钩子,如下所示:
import React, {useState} from "react";
import {Text} from "react-native";
const Welcome = () => {
const [name, setName] = useState('World!');
return <Text>Hello, {name}</Text>;
}
哇塞!这看起来比之前的例子干净多了!我们仍然有一个能够持有和管理状态变化的组件,但它比有状态类组件要短得多。我还觉得这种组件的逻辑流程非常好,我们可以在一行中声明状态值和状态设置函数。
如果你想看到这段代码的实际应用,你可以访问snack.expo.dev/@p-syche/example-of-functional-component-with-usestate。
这是一个Expo Snack——类似于网络开发的代码片段。
你应该了解哪些钩子?
我们之前讨论的第一个钩子是useState,这是你应该首先熟悉的一个。第二个最常用的钩子是useEffect。我也相信这是命名最好的钩子之一。你可以用它给你的组件添加各种副作用。你可能会问,“什么是副作用?”亲爱的读者,让我们通过例子来尝试理解这个概念:想象一个社交媒体应用(就像我们在本书中将要构建的应用一样!)现在,让我们想象你被分配了一个添加点赞计数器的任务。你有一个带有计数的父<Text>组件。它看起来可能像这样:
const LikesParentComponent = () => {
const getCounterNumberFromApi = someFunctionRetrievingDataFromAPI();
const [counterNumber, setCounterNumber] = useState(getCounterNumberFromApi)
return (
<LikesComponent counterNumber={counterNumber} />
);
};
const LikesComponent = (counterNumber) => {
const [likeState, setLikedState] = useState ("haven't yet liked");
return (
<View>
<Text>you {likeState} this post</Text>
<Pressable onPress={setLikedState("liked")}>
<Text>Press here to check/uncheck</Text>
</Pressable>
<Text>{counterNumber} other people liked this post</Text>
</View>
);
};
我们正在将counterNumber从<LikesParentComponent>作为属性传递。让我们假设这个父组件使用名为someFunctionRetrievingDataFromAPI()的非常贴切的功能来从 API 获取点赞数。
到目前为止,看起来相当不错,对吧?我们加载了组件;它们从 API 中检索点赞数据并将其传递给我们的 <LikesComponent>,它以很好的方式显示。但是等等!如果用户触摸了 <Pressable> 组件会发生什么?我们将 <Text> 设置为 liked,但计数器不会增加!我们绝对不能就这样留下!这是一个经典的副作用:用户操作需要组件状态中的额外更改。首先,我们不能在 <LikesComponent> 内部更改 counterNumber,因为我们之前在状态和属性部分学到的是属性是不可变的。那么我们能做什么呢?我们可以使用父组件的状态设置函数。这个函数可以作为属性传递。这意味着 <LikesParentComponent> 将像这样调用其子组件:
<LikesComponent counterNumber={counterNumber} setCounterNumber={setCounterNumber} />
到目前为止,一切顺利。现在,我们只需要在适当的时候调用这个设置函数,这意味着在 <LikesComponent> 中按下按钮时。这就是使用 useEffect 钩子时的样子:
const LikesComponent = (counterNumber, setCountNumber) => {
const [likeState, setLikedState] = useState ("haven't yet liked");
useEffect(() => {
if (likeState === "liked") {
setCounterNumber(counterNumber++)
}
else {
setCounterNumber(counterNumber-1)
}
}, [likeState])
return (
<View>
<Text>you {likeState} this post</Text>
<Pressable onPress={setLikedState("liked")}>
<Text>Press here to check/uncheck</Text>
</Pressable>
<Text>{counterNumber} other people liked this post</Text>
</View>
);
};
如您可能注意到的,useEffect 钩子看起来与 useState 钩子非常不同。不必过于担心这一点。这两个钩子是最常用的,您会习惯它们的构思和消费方式。
我们示例中的 useEffect 钩子内部是一个常见的 if/else 语句,用于检查状态的值是否等于 "liked"。这个钩子最关键和有趣的部分是位于最后的数组。这个数组被称为依赖数组。它用于通知钩子函数何时运行。在我们的例子中,当 likeState 的值发生变化时,useEffect 钩子应该运行。
useEffect 钩子可以用来更新应用的不同部分,帮助数据获取、用户驱动的交互等等。这个钩子非常强大,但它有一个非常大的风险:如果编写不当,它会导致许多重新渲染。
关于 useEffect 最重要的事情要记住
确保正确设置 useEffect 的依赖数组!
正如您可能在官方 useEffect 的依赖数组中找到的那样。如果我们设置它,那么我们的效果将仅在依赖数组中的任何项目发生变化时运行。
还有几个其他的内置钩子。在开始编写 useState 和 useEffect 时,您不必了解它们的所有内容——这足以让您开始。当您到达这两个钩子不足以满足需求的时候,您可以回到 ReactJS 文档中阅读关于其他钩子的内容。您还可以编写适用于您特定应用的自己的自定义钩子。
现在我们知道了什么是钩子以及为什么我们要使用它们,让我们开始设置我们的示例应用!
设置示例应用
啊!您可能一直在等待的时刻:实际上创建一个应用!
我们将首先准备我们的开发环境。你需要一个 集成开发环境(IDE)例如 VS Code、Sublime Text、Atom 或你可能喜欢的任何其他东西。IDE 是你编写 React Native 代码所需的一切。但我们还需要一种方式来查看代码的渲染效果,不是吗?
在网页开发的情况下,我们只需使用浏览器来查看和测试我们的代码。然而,React Native 应用不能在网页浏览器中轻松测试。它们可以在真实或模拟设备上进行测试。在理想情况下,你会拥有多部手机,并将它们通过 USB 插入电脑以查看你的应用。尽管如此,我们大多数人并没有多部手机。这就是为什么我们可以使用手机模拟器。在移动世界中,有两个主要玩家:Android 和 Apple。由于 Android Studio 应用,Android 模拟器几乎可以在任何桌面平台上使用。不幸的是,iPhone 模拟器只能在 Mac 电脑上运行。
设置模拟器可能是一项艰巨的任务,但不必过于担心!有 Expo 帮助!
我在第一章中提到了 Expo。如果你跳过了那部分,让我快速介绍一下:Expo 是 React Native 开发工具。它使得构建、测试和发布应用变得更加容易。Expo 是在 React Native 之上的一层包装,旨在使开发者体验更加流畅。
环境设置
让我们确保你的开发环境已经准备好。根据 Expo 网站上的说明,你需要最新的 Node、Git 和 Watchman。所有这些的链接都可以在 Expo 的文档中找到,地址是 docs.expo.dev/get-started/installation/。我们在开发过程中将使用 Yarn,所以请确保你已经安装了它。你可以在这里找到详细说明:classic.yarnpkg.com/en/docs/install。一旦你浏览了这些链接,请按照以下步骤操作:
-
当你准备好时,请继续安装 Expo 的 CLI 工具:
$ npm install –global expo-cli -
通过运行
expo whoami来验证安装是否成功。你还没有登录,所以你会看到expo register,或者你可以使用expo login登录现有的账户。 -
下一步是在你的手机上安装 Expo Go 应用。你可以在 Android 商店中找到它,地址是
play.google.com/store/apps/details?id=host.exp.exponent,以及在 App Store 中的apps.apple.com/app/expo-go/id982107779。
多亏了 Expo,无论你使用的是 Mac 电脑还是 Windows 电脑,以及你拥有什么类型的手机,Expo Go 应用都会“自动”在 Android 和 Apple 设备上运行。
-
我们已经准备好了——是时候创建应用了。打开你的终端并运行以下命令:
$ npx create-expo-app funbook-app -
当被提示选择模板时,请选择 空白。
你可以为你的应用选择任何你喜欢的名字。我建议使用“Funbook”,因为它听起来有点像“Facebook”,我们将创建一个社交媒体应用克隆。保持和我一样的名字可能会使跟随代码示例更容易。
-
应用初始化成功运行后,你可以通过运行以下命令进入你的应用文件夹:
$ cd funbook-app -
然后运行开发服务器,如下所示:
$ expo start
或者,如果你使用 Yarn,运行这个命令:
$ yarn start
Expo CLI 启动 Metro Bundler,这是一个编译我们应用 JavaScript 代码的 HTTP 服务器。你应该会看到一个二维码,你现在可以使用手机上的 Expo Go 应用扫描它。你可以在你想要的任何设备上运行你的 Funbook 应用。
应用开发一开始可能会显得有些令人畏惧,但如果第一次尝试不是所有东西都完美运行,请不要担心。有很大可能性你会在终端窗口中找到罪魁祸首。终端输出是你获取信息的最佳来源。
如果你看到终端中有任何错误,或者感觉有点迷茫,请确保查看 Expo 安装文档:docs.expo.dev/get-started/create-a-new-app/。
我设置了一个公共存储库,我们将在这本书的整个过程中使用它。你可以在这里找到它:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native。
在这个存储库的main分支上,你会找到一个已经设置好的应用。你可以随意克隆或分叉这个存储库。记住,如果你想在你电脑上运行这个应用,你仍然需要安装node、watchman和yarn)。
应用结构
让我们考虑一下我们需要哪些界面和组件来构建一个简单的社交媒体应用。在这里,“界面”指的是在网页开发中通常所说的“页面”。这是应用的一个大块,由许多组件组成,一起呈现在屏幕上。
我们的应用肯定需要一个登录界面、一个社交媒体信息流界面和一个个人资料界面。我们还会添加一个包含收藏帖子的屏幕,以及一个用户可以添加他们帖子的屏幕。我们将为信息流和个人资料使用假数据,并为登录使用单个用户名和密码。我们不会实现注册流程,以便保持事情简单。
我们想专注于数据流,所以我们将使用一个免费的社交媒体 UI 工具包来“移除”设计,换句话说。以下是我们将使用的设计文件链接:www.pixeltrue.com/free-ui-kits/social-media-app。
应用根目录
我们的应用将至少包含五个界面,这意味着我们需要设置导航以便在那些界面之间移动。用户将从登录界面开始。他们将填写他们的信息,然后将被重定向到社交媒体信息流界面。
显然,我们需要一种方式让我们的用户在应用中移动。最常用的导航库之一叫做React Navigation。这是一个专门为React Native应用创建的库。它提供了三种类型的导航:抽屉导航、标签导航和堆栈导航。抽屉导航是指你应用侧边有一个小抽屉,里面包含指向应用中不同位置的链接。标签导航将显示标签(底部或顶部),包含指向不同位置的链接。堆栈导航就像一堆卡片——每个屏幕都是一个卡片,具有重定向到任何其他卡片的能力。如果你想了解更多关于这个库的信息,你可以在进一步阅读部分找到文档链接。
现在还有其他导航库,但 React Navigation 无疑是React Native社区中最受欢迎的一个。它也正在积极维护和更新,以与最新的React Native版本兼容。
我们需要首先将库作为依赖项添加到我们的项目中。为此,请按照以下步骤操作:
-
我们可以通过运行以下命令来添加库:
$ yarn add @react-navigation/native -
如果你访问文档网站,你会注意到有针对“Expo管理项目”和“裸React Native项目”的不同 CLI 命令。请确保遵循 Expo 管理项目的说明。在我们的情况下,我们需要运行以下命令:
$ expo install react-native-screens react-native-safe-area-context -
我们首先需要显示一个登录界面,这将把我们的用户重定向到主应用屏幕。为了做到这一点,我们将使用一个堆栈导航器。让我们将它的依赖项添加到我们的项目中,具体如下所示,请参阅
reactnavigation.org/docs/stack-navigator/:$ yarn add @react-navigation/stack$ expo install react-native-gesture-handler
堆栈导航器的最后一步设置是在我们的App.js文件的最顶部导入手势处理库。
堆栈导航器将非常有用,可以管理我们应用的登录状态,但一旦用户登录,我们还需要底部标签导航来在其他屏幕之间切换。标签导航对应用用户来说感觉非常自然。它在所有屏幕上都是可见的,使得使用应用变得容易。
就目前而言,我们只需要运行一个命令:
$ yarn add @react-navigation/bottom-tabs
此命令将底部标签导航作为依赖项添加到我们的项目中,这样我们就可以稍后使用它了。
你可能会想知道为什么我们需要分别添加这么多不同的依赖项。这是由于React Navigation的作者决定如何构建他们的库。他们确信大多数人不需要他们应用中的每一种导航,那么为什么他们应该在应用包中包含它呢?每个库用户都可以决定React Navigation的哪一部分对他们有用,并只包含那一部分。
让我们继续为我们的基本应用添加一些结构。每个应用至少由几个不同的界面组成,而这些界面又是由组件构成的。我们的基本社交媒体克隆应用需要一个登录界面和一个主界面,登录后可见。由于我们正在创建一个社交媒体应用,我们将主界面命名为“Feed”,因为它将包含用户的新闻源。随着我们的进展,我们肯定会添加更多界面,但这两个将是一个良好的起点。
设置界面
登录界面需要一个用户名输入字段、一个密码输入字段以及一个登录按钮。但到目前为止,我们将创建一个包含一些文本的虚拟组件。
我们将首先创建登录界面。您可能会想知道“创建一个界面”是什么意思。我的意思是,一些组件将是整个应用界面的包装器。有些人喜欢称它们为屏幕,在 Web 开发中,您会称它们为站点或页面。从编程的角度来看,它们就像任何其他组件一样是组件。但我们决定,从逻辑上讲,它们代表应用的一个更大的部分,我们将它们放在一个特殊的文件夹中,称为 surfaces。
这是我们的登录界面:
// ./src/surfaces/Login.js
import React from "react";
import { View, Text } from "react-native";
export const Login = () => {
return (
<View>
<Text>this will be the login screen</Text>
</View>
);
};
如您所注意到的,它实际上是一个名为 Login 的虚拟组件,放置在 surfaces 文件夹中。
使用相同的逻辑,我们将创建一个 Feed 界面,用户登录后应该显示:
// ./src/surfaces/Feed.js
import React from "react";
import { View, Text } from "react-native";
export const Feed = () => {
return (
<View>
<Text>this will be the feed screen</Text>
</View>
);
};
我们已经准备好了应用的两个基本部分;现在我们需要将它们组合起来。这就是 React Navigation 发挥作用的地方。
每个 React Native 应用都需要一个根文件,就像每个网站都需要在根目录下的 index.html 文件一样。这个根文件通常被称为 App.js。这是显示任何和所有内容的真相来源(SOT)。您可以将其视为一棵树的树干,从它那里长出了许多分支。在这个比喻中,分支是不同的应用界面。您明白了吗?我相信您一定明白了!您很聪明!毕竟,您正在阅读我的书。
让我们设置父组件以显示正确的流程——首先,登录界面,然后,内容流:
// ./App.js
import 'react-native-gesture-handler';
import React, { useState } from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import { createBottomTabNavigator } from "@react-navigation/ bottom-tabs";
import { Login } from "./src/surfaces/Login";
import { Feed } from "./src/surfaces/Feed";
const Stack = createStackNavigator();
const Tab = createBottomTabNavigator();
function Home() {
return (
<Tab.Navigator>
<Tab.Screen name="Feed" component={Feed} />
</Tab.Navigator>
);
}
export default function App() {
const [userLoggedIn, setIsUserLoggedIn] = useState(true);
return (
<NavigationContainer>
<Stack.Navigator>
{!userLoggedIn ? (
<Stack.Screen name="Login" component={Login} />
) : (
<Stack.Screen
name="Home"
component={Home}
options={{ headerShown: false }}
/>
)}
</Stack.Navigator>
</NavigationContainer>
);
}
您可以在以下 Expo Snack 中找到前面的代码:snack.expo.dev/@p-syche/simplifying-state-management---chapter-2-example-3。
在前面的代码中,您会注意到我们使用了 useState 钩子。这样,我们很容易地为我们的函数式 App 组件添加状态。我们将初始状态设置为 false——用户首次打开应用程序时不应登录。当用户登录时,他们将被重定向到我们的堆栈中的第二个“卡片”。这个“卡片”是 Home 组件。这是一个包装组件,用于包含我们应用程序的更大部分:除了 Login 之外的所有带有标签底部导航的表面。如您所注意到的,导航器是嵌套的:标签导航嵌套在堆栈导航器中。这在 React Native 应用程序中是一种常见且实用的做法。您可以在 React Navigation 文档中了解更多关于嵌套导航的信息:reactnavigation.org/docs/nesting-navigators。
就这样!我们使用 Expo 设置了一个应用程序。我们添加了代表应用程序未来表面的多个组件。我们还添加并配置了 React Navigation 库。我们的应用程序现在看起来不太美观,但它应该能工作。您可以通过 Expo Go 应用程序在您的手机上查看它,或者在计算机屏幕上的手机模拟器中查看。
我在 GitHub 上设置了一个公共仓库,以便您,亲爱的读者,可以更轻松地跟随本书中展示的代码片段和示例。您可以在以下位置找到该仓库:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native。请随意克隆或分叉它。main 分支包括基本的应用程序设置。每个状态管理库的实现都在不同的分支上。我们将随着前进讨论细节。如果您决定使用这个仓库,您会注意到 UI 工具包中的样式已经实现。本书中我们将不会专注于样式,但它是对任何应用程序的一个很好的补充。
摘要
我们在这里做了一些真正的好工作!我们开始时查看了一些简单的代码示例,这些示例对于理解一些 ReactJS 编程概念是必要的,例如组件状态和属性、生命周期方法和钩子。理解并内化状态和属性、有状态和无状态组件之间的差异非常重要。对这些概念的良好掌握可以决定您的应用程序是否能够顺利运行。
在深入研究重要的 React 概念和示例之后,我们开始实际设置我们的应用程序。这是一个非常激动人心的时刻!我们有了基础,我们准备构建一个真实的社交媒体克隆应用程序。在下一章中,我们将熟悉预览和调试我们的应用程序。我们将设置所有必要的表面,我们将添加示例数据,最后我们将为应用程序添加样式。我迫不及待了!
进一步阅读
-
React Native 的文档——具有状态的组件示例:
reactnative.dev/docs/intro-react#state. -
状态与属性:
https://lucybain.com/blog/2016/react-state-vs-pros/.
github.com/uberVU/react-guide/blob/master/props-vs-state.md.
- 将生命周期方法添加到类中—ReactJS 文档:
https://reactjs.org/docs/state-and-lifecycle.html#adding-lifecycle-methods-to-a-class.
- 关于 hooks 的完整博客文章:
https://pl.reactjs.org/blog/2019/02/06/react-v16.8.0.html.
- ReactJS 关于 hooks 的文档:
https://reactjs.org/docs/hooks-reference.html#useeffect.
- React Navigation 文档:
https://reactnavigation.org/docs/getting-started/.
-
React Navigation—底部标签导航:
reactnavigation.org/docs/tab-based-navigation. -
React Navigation 关于身份验证流程的指南:
reactnavigation.org/docs/auth-flow. -
React Navigation 关于嵌套导航器的指南:
reactnavigation.org/docs/nesting-navigators.
第二部分 – 创建一个真实、可工作的应用
在本部分中,我们将专注于构建一个真实、可工作的移动应用。读者将学习如何规划应用功能并配置 Funbook 应用的真正设置;然后,他们将学习如何为 React Native 应用添加样式,使其与给定的设计相匹配,以及如何引入真实数据。
本部分包括以下章节:
-
第三章, 规划和设置 Funbook 应用
-
第四章, 为 Funbook 应用添加样式和内容
第三章:规划和设置 Funbook 应用
在上一章中,我们学习了如何设置 React Native 应用。我们遵循的步骤,安装依赖项、构建和运行应用,对于您可能想要构建的大多数应用都是通用的。现在,是时候关注我们将在这本书中构建的应用的具体细节了。我们想要创建一个社交媒体克隆应用,以便我们可以比较该应用中不同的状态管理解决方案。在本章中,我们将仅使用 React Native 内置解决方案(状态、属性、钩子和上下文)规划和构建我们的示例应用。我们将采取以下步骤:
-
规划所需界面和组件
-
在应用中规划数据流
-
舒适地预览和调试应用
到本章结束时,您将很好地了解如何规划 Funbook 应用的开发工作。您还将了解到如何舒适地与 React Native 应用一起工作。
技术要求
为了跟随本章内容,您需要具备一些 JavaScript 和 ReactJS 的知识。如果您已经跟随了本书的前两章,您应该能够无任何问题地继续前进。
您可以自由选择您喜欢的 IDE,因为 React Native 不需要任何特定功能。目前,前端开发者中最受欢迎的 IDE 是微软的 VSCode、Atom、Sublime Text 和 WebStorm。
您可能已经遵循了上一章中的设置指南。如果您没有设置自己的应用,您可以从以下地址克隆为本书专设的存储库:
https://github.com/PacktPublishing/Simplifying-State-Management-in-React-Native.
在这个存储库中,您将找到一个非常基础的应用,正如它在上一章中设置的那样。您还将找到以章节名称命名的文件夹。不出所料,每个文件夹都包含一个描述在给定章节中所述的 Funbook 应用的版本。
规划所需界面和组件
正如我之前提到的,我们可以将我们的应用分为界面,然后将界面分解成更小、可重用的组件。我们的应用将需要以下界面:
-
登录
-
动态(这同样是我们的主页界面)
-
添加帖子
-
收藏
-
个人资料
我们将这些界面作为项目中的文件设置。让我们快速查看我们将为应用使用的免费设计文件。您可以在以下位置找到文件:www.pixeltrue.com/free-ui-kits/social-media-app。
您可以下载此文件并在 Figma 中打开它,或者将其导入到www.figma.com。如果您还没有 Figma 账户,请不要担心,它们是免费的。您现在可以花点时间查看实际文件,或者如果您对截图就足够了,我们就一起看看:

图 3.1 – Figma 网站上的设计模板
让我们放大查看主页:

图 3.2 – 主页表面的设计
你可能已经注意到设计底部标签栏中有五个项目。我们遗漏了哪一个?聊天气泡。让我们继续将这个表面添加到我们的应用中。我鼓励你亲自添加这个文件,然后回到这里对照我的例子进行检查。以下是我的Conversations表面目前的样子:
import React from "react";
import { View, Text } from "react-native";
export const Conversations = () => {
return (
<View>
<Text>this will be the chat screen</Text>
</View>
);
};
下面是带有新添加屏幕的App.js文件:
import "react-native-gesture-handler";
import React, { useState } from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import { createBottomTabNavigator } from "@react-navigation/ bottom-tabs";
import { Login } from "./src/surfaces/Login";
import { Feed } from "./src/surfaces/Feed";
import { Profile } from "./src/surfaces/Profile";
import { Favorites } from "./src/surfaces/Favorites";
import { AddPost } from "./src/surfaces/AddPost";
import { Conversations } from "./src/surfaces/Conversations";
const Stack = createStackNavigator();
const Tab = createBottomTabNavigator();
function Home() {
return (
<Tab.Navigator>
<Tab.Screen name='Feed' component={Feed} />
<Tab.Screen name='Conversations' component={Conversations} />
<Tab.Screen name='AddPost' component={AddPost} />
<Tab.Screen name='Favorites' component={Favorites} />
<Tab.Screen name='Profile' component={Profile} />
</Tab.Navigator>
);
}
[…]
好的!到目前为止看起来不错!
现在我们已经设置了主要表面,让我们尝试分析哪些元素是可重复使用的组件的良好候选者。
回顾设计文件,让我们从主页表面开始。在顶部,我们看到一个横向的头像列表和下面的重复卡片列表。每个卡片都有一个作者图像、标题、收藏数量和对话数量。因此,主页组件应该由头像和卡片组件构建。
接下来是对话屏幕:它由一个搜索栏和一张显示对话中人的名字和最后一条交换的消息的卡片列表组成。当点击消息时,我们将进入 Figma 文件中名为消息的屏幕,在那里我们将看到一个更大的头像、一条消息列表和一个输入框。记住我们已经在主页上有头像了;让我们看看我们是否可以重用头像组件。也许只能在一定程度上,因为主页头像、对话头像和消息头像的样式并不相同。它们都是圆形图像,但它们的边框和大小不同。也许我们可以创建一个接受大小和边框样式作为属性的头像组件。这是一个相当不错的想法!当我们开始编写代码时,我们将尝试实现这一点。
在我们的免费设计文件中,我们将要详细设计的最后一个表面是个人资料。这里还有一个头像;这个头像甚至不是圆形的。它后面跟着用户名、一些统计数据和一个两列的图片和书签列表。由于我们不会实现书签功能,我们将用收藏夹替换设计中的书签。你可能注意到两列是用两种不同样式的元素构建的,我们可能也应该这样创建我们的组件:一个用于图片列的卡片组件,一个用于收藏夹卡片列的组件。
最后但同样重要的是:底部的标签栏。我们的设计文件包括四个常规图标和一个不同风格的图标。对 React Navigation 组件进行样式化是一个完全不同的任务,因为我们需要阅读文档来找出如何实现自定义图标、激活和未激活样式,以及自定义样式。
由于我们使用的是免费的设计文件,它并没有涵盖我们想要创建的所有表面。我很高兴我们手头有这个免费资源,我们将尝试使用通用样式和组件来确定剩余的两个表面应该是什么样子。
登录表面当然应该包括两个输入:用户名和密码。我们将重用 Figma 上消息屏幕上的可见输入和启动屏幕的背景。至于添加帖子的表面所需的表面,我们将有一个与主页表面匹配的圆形方形图像,以及帖子标题的输入。
让我们总结一下我们的计划:我们已经创建了所有表面。接下来,我们将创建为表面所需的组件。我们将创建一个头像组件,用于主页、对话和消息表面。我们将为主页表面创建一个卡片组件。然后,我们将为对话表面创建另一个卡片组件,以及一个搜索框组件。我们需要将导航连接起来,以便正确地从对话切换到消息。在消息表面上,我们将重用头像组件、用于显示消息的组件以及一个可重用的输入组件。接下来,我们将转到个人资料屏幕,创建个人资料头像组件、个人资料统计数据组件以及图像卡片组件和收藏物品卡片组件的不同组件。然后,我们将使用之前为消息屏幕创建的输入框组件来组合登录屏幕。最后,我们将完成添加帖子表面,使用主页表面卡片和输入的版本。我不建议事先创建所有文件,因为在创建实际组件的过程中,很多东西可能会发生变化。
在开始编写组件之前,让我们尝试分析我们的应用需要哪些数据。
在应用中规划数据流
这通常是前端开发者职责之外的应用开发部分。客户通常会确定他们想要的数据,这些数据由后端开发者组织。然而,如果您能参与数据流组织的规划,您将使您未来的工作更加容易。鉴于我们只使用示例数据构建应用的前端,我们可以自由地组织它。
我们将再次使用设计文件,作为确定需要完成的工作的基础。从主页屏幕开始,我们知道我们需要一个用户列表和一个要在主页表面上显示的项目列表。至于对话表面,我们需要一个包含相应用户名和消息的对话列表。我们还需要每个对话的数据,以便我们可以在消息表面上显示它。在个人资料表面上,我们需要与用户相关的数据列表(姓名、头像图像、统计数据),以及两个图像列表:添加的图像和喜欢的图像。至于设计中缺失的表面,我们需要登录屏幕的登录名和密码。对于添加帖子表面,我们不需要任何示例数据。
使用真实数据工作可以更容易地可视化应用和特定组件的未来形态。这就是为什么我设置了书籍仓库的 GitHub 页面来保存我们的示例数据。您可以在 GitHub Pages(https://packtpublishing.github.io/Simplifying-State-Management-in-React-Native/)或主书库的docs/文件夹中找到它们:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/docs。
浏览示例数据
您可以在任何时候查看应用中使用的示例数据。请查看主仓库的数据分支:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/blob/data/docs/index.md,并在docs/文件夹中查找。您可以复制任何您想要的内容到您自己的项目中。
我们需要的数据拼图中最大、最明显的一部分是用户列表。您可以在 GitHub 上查看该文件:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/blob/main/docs/users.json。我们的应用将消费原始 JSON 文件,可以通过以下链接访问:raw.githubusercontent.com/PacktPublishing/Simplifying-State-Management-in-React-Native/main/docs/users.json。
您可能会想知道,如果我们正在构建一个使用示例数据的简单应用,为什么还要添加用户 ID。原因是我们将使用用户数据作为key属性上的头像列表。从理论上讲,我们可以使用图片 URL 作为我们的唯一键,然后尝试记住不要为多个人使用相同的图片。然而,使用 ID 是一个更干净、更接近真实世界应用的做法。
现在我们有了用户列表,让我们看看一个特定用户配置文件可能是什么样子。我们的用户需要一个 ID,这个 ID 应该与users.json文件中他们名字的记录相匹配。他们还有一个名字和头像图片 URL。我们需要知道给定用户有多少帖子、关注者和被关注者。最后,我们需要两个图片列表:添加的和喜欢的图片。看看john_doe.json文件——这就是我们的示例用户配置数据的样子。
接下来,我们转到users.json文件,以显示头像列表,因此我们在这里不需要添加任何额外的头像列表数据。接下来将是一个以卡片形式显示的物品列表,其中包含图片。示例数据可以在home.json文件中找到。
让我们为对话创建一个示例数据集。它并不复杂;它包括用户名、用户头像 URL、消息和 ID。我们需要对话 ID 来正确地在消息界面显示对话详情。
最后,我们应该为messages创建示例数据。在这个文件夹中,我们将为对话创建几个文件。每个文件都按对话 ID 命名,这将使数据检索更容易、更易读。
至于登录界面,我们将使用一个非常小的 JSON 文件,其中将包含用户名和密码。当登录表单正确或错误地填写时,我们将使用这些数据来创建用户流程。
查看 JSON 文件,你会注意到一些数据在几个文件中重复;具体来说,是用户 ID、用户名和头像图像 URL。在现实世界的应用程序中,这可能会在未来引起问题,即更新应用程序中的数据将不会正确更新或在其他地方可用。这就是为什么我们将删除对用户名和头像图像的所有引用,只留下用户 ID,我们将使用它从users.json文件中获取其他数据。
就这样!我们有一个大型的用户列表,我们将在应用程序的不同部分使用,包括主页界面的数据、个人资料界面和对话。我们准备好创建我们的组件了!对吧?对!然而,我们首先需要熟悉预览和调试我们的应用程序。
熟悉预览和调试应用程序
你是否在查看你的代码是否在设备或模拟器上正确运行?如果不是,让我们看看你如何查看它。你需要做的第一件事是在你的终端中运行这个命令:
$ yarn start
当expo完成设置你的开发服务器后,你可以按“i”为 iPhone 模拟器(如果你在 Mac 电脑上工作),按“a”为 Android 模拟器(如果你已安装 Android Studio),或者你可以拿起你的手机并使用 Expo Go 应用程序。
无论你选择哪个,你都会在你的设备上自动打开一个浏览器窗口。这个浏览器窗口看起来是这样的:

图 3.3 – 浏览器中的 Expo 开发者工具
如果你想在手机上查看你的应用程序,你将在这里找到 Expo Go 应用程序中的扫描二维码。你将在这里看到错误消息;你甚至可以使用这个页面来发布你的应用程序。
我喜欢在 iPhone 模拟器打开的情况下工作。这是我电脑上设置的应用程序的外观:

图 3.4 – iPhone 13 与 iOS 15.2 模拟器截图
希望你能看到类似的东西。如果你没有,你总是可以克隆 GitHub 仓库,或者比较你的代码与已发布的代码。你前面截图中所看到的 app 状态应该是位于此处仓库的main分支上应看到的状态:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native。
花些时间在应用上玩玩。尝试创建一些明显的错误,比如在<Text />组件外写入纯文本,可能使用<div>标签,或者没有关闭标签。
我们将在底部标签导航上进行代码更改练习。我们不会为那个创建任何组件。
通过在设置导航器时设置属性,可以自定义标签导航的外观。我们还可以添加一些特定的屏幕选项。我们的底部标签导航将使用图标作为标签,因此我们需要首先将图标库导入到主App.js文件中。我们将使用名为@expo/vector-icons的库。这个库默认安装在所有使用expo初始化的项目中。
添加库
在添加任何额外的依赖和库之前,请确保检查 Expo 文档,看看你想要的库是否已经安装。如果你确实需要添加某些内容,请确保添加与 Expo 工作流程兼容的库。
Expo 已经为我们做了所有繁重的工作;我们手头有一个庞大的图标库。我们所需做的就是使用它来为我们的导航器添加图标。我们将从为五个项目中的四个添加简单图标开始:
import Ionicons from "@expo/vector-icons/Ionicons";
// …
function Home() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === "Feed") {
iconName = focused ? "md-home" : "md-home-outline";
} else if (route.name === "Conversations") {
iconName = focused ? "chatbox" : "chatbox-outline";
} else if (route.name === "Favorites") {
iconName = focused ? "heart" : "heart-outline";
} else if (route.name === "Profile") {
iconName = focused ? "person-circle" : "person-circle-outline";
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: "#25A0B0",
tabBarInactiveTintColor: "#000000",
})}
>
<Tab.Screen name='Feed' component={Feed} />
<Tab.Screen name='Conversations' component={Conversations} />
<Tab.Screen name='AddPost' component={AddPost} />
<Tab.Screen name='Favorites' component={Favorites} />
<Tab.Screen name='Profile' component={Profile} />
</Tab.Navigator>
);
}
我们在<Tab.Navigator>中添加了一个简单的if语句,其中我们给出了关于应该显示哪个组件的特定指令。然而,每次我们显示来自@expo/vector-icons库的<Ionicons>组件时,我们都在给它提供不同的属性。现在我们将保留AddPost项。一旦我们创建了一个可重用的按钮组件,我们就会回到这里并添加它。
我们现在可以进一步自定义的是tabBar标签。根据设计,标签不应显示。我们需要向<Tab.Navigator>添加另一个属性:
// …
tabBarInactiveTintColor: "#000000",
tabBarShowLabel: false,
// …
看起来不错!现在,关于头部呢?我们的应用有一个非常通用的头部,背景为白色,标题为给定表面的标题。正如你在设计中所看到的,一些表面没有标题(例如<Tab.Navigator>):
// …
tabBarInactiveTintColor: "#000000",
tabBarShowLabel: false,
headerTransparent: true,
// …
哈哈!这成功了——但是等等,屏幕上显示的文本现在在固定的透明头部后面!

图 3.5 – iPhone 模拟器显示 UI 问题
我们需要确保我们应用的内容永远不会像这样飞离屏幕。这是一个不容易完成的任务,尤其是在有这么多屏幕形状、凹口和数字按钮的情况下。幸运的是,React Navigation 的制作者添加了一个名为 <SafeAreaView> 的包装组件。我们必须在 <NavigationContainer> 周围添加 SafeAreaProvider 组件。该组件在“幕后”使用 React Context。为了使用此上下文,我们需要在每个表面周围添加 <SafeAreaView>。主应用组件将看起来是这样的:
export default function App() {
const [userLoggedIn, setIsUserLoggedIn] = useState(true);
return (
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator>
// …
</Stack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
}
让我们在 <Feed> 组件周围添加 <SafeAreaView>。您看到有任何改进吗?没有?那是因为还有一个需要注意的地方:我们需要在包装组件中添加 {{flex: 1}} 样式。好吧,表面看起来更好了——文本被包含在屏幕上——但它仍然在标题后面…

图 3.6 – iPhone 模拟器 UI 变更的特写
我们希望给表面的顶部添加填充,这样我们的内容就会在标题下方开始。我们希望确定标题的高度,而无需硬编码任何像素值。使用 useHeaderHeight()。现在的 Feed 组件看起来是这样的:
import React from "react";
import { SafeAreaView } from "react-native-safe-area-context";
import { View, Text } from "react-native";
import { useHeaderHeight } from "@react-navigation/elements";
export const Feed = () => {
const headerHeight = useHeaderHeight();
return (
<SafeAreaView style={{ flex: 1, paddingTop: headerHeight }}>
<View>
<Text>this will be the feed screen</Text>
</View>
</SafeAreaView>
);
};
应用应该看起来像这样:

图 3.7 – 固定 UI 的 iPhone 模拟器
确保在跟随本书学习时,将 <SafeAreaView> 添加到所有表面。如果您更喜欢在 GitHub 上查看代码更改,您可以在名为 chapter-3 的分支上找到它们:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-3.
如果您想知道为什么我们要在 <Tab.Navigator> 中添加标题样式而不是根组件,我邀请您查看我们应用根部的 <Stack.Navigator>,为 <Stack.Screen> 组件做准备,您会注意到以下选项:
options={{ headerShown: false }}
我们正在告诉 React Navigation 隐藏 <Stack.Navigator> 的标题并显示嵌套的 <Tab.Navigator> 的标题。这个嵌套的 <Tab.Navigator> 也是我们需要样式的。继续更改您项目中的 headerShown 选项并观察会发生什么。您应该在应用中看到另一个带有“主页”标题的标题!那是因为我们将 Home 命名为主要父组件,用于创建 <Tab.Navigator>。确保在回到我们的应用工作之前,将 headerShown 选项改回 false。
我希望你在应用中修改和预览更改方面感到越来越舒适。让我们通过添加自定义字体来完成本节。我们再次使用 Expo 提供的库:Expo Google Fonts。如果你快速查看设计文件,你会找到所使用的字体名称,它是一个名为 Poppins 的 Google 字体。
我们将把字体导入到Feed组件中,将其作为style属性添加到<Text>组件中,然后……哦不!问题!

图 3.8 – iPhone 模拟器显示错误
尽管这个巨大的红色框似乎在向我们尖叫,但无需担心。我们只需要阅读错误信息。它指出@expo-google-fonts/poppins未定义。当然!我们需要在我们的项目中安装这个字体。让我们在终端中运行以下命令:
$ expo install expo-font
$ expo install @expo-google-fonts/poppins
错误应该已经消失了。现在,我们可以安全地将我们的字体家族添加到<Text>组件中。或者我们可以吗?


图 3.9 – iPhone 模拟器显示错误提示消息和详细信息
字体尚未加载……让我们回到 Expo 文档中,确保我们正确地加载了所有内容。
根据文档说明,我们首先需要在根组件周围使用带有AppLoading包装器的useFont钩子!以下是我们需要添加到App.js文件中的内容:
export default function App() {
const [userLoggedIn, setIsUserLoggedIn] = useState(true);
let [fontsLoaded] = useFonts({
Poppins_400Regular,
});
if (!fontsLoaded) {
return <AppLoading />;
}
return (
// …
现在我们就完成了。现在,应用可以正确运行,我们可以在任何我们想要的地方添加fontFamily样式:
<Text style={{ fontFamily: "Poppins_400Regular"}}>
在本节中,我们熟悉了修改代码、预览我们的应用和处理错误。现在,我们准备好在下一章中编写和样式化组件。
摘要
在本章中,我们规划了我们的应用并熟悉了预览和调试它。这两个步骤对于创建良好的开发者体验至关重要。首先,我们不希望遇到任何重大惊喜——这就是为什么我们要提前规划。你可以将这比作建筑物的建造过程。没有自尊的施工工人会在制作或至少查看蓝图之前开始搭建墙壁和门。我们作为软件开发者,正在构建一个数字产品而不是建筑物,但我们使用“构建”这个词是非常有原因的。
其次,我们需要了解如何检查我们所写的代码是否真的在起作用。你的代码可能在你看来逻辑清晰,但这并不意味着在 JavaScript 尝试理解你的逻辑后它仍然会工作。这就是为什么每个网页开发者都会在工作的同时打开浏览器窗口,以及为什么移动应用开发者需要查看手机或手机模拟器。由于我们将花费相当多的时间在手机上查看我们的应用,所以让自己感到舒适是很重要的。
现在,亲爱的读者,我们准备好继续深入 React Native 的细节之旅了!在下一章中,我们将构建我们上面计划中的组件。我们还将添加样式以匹配我们的美丽设计。我们将遇到一些 React Native 的经典问题以及一些怪癖——最终我们将拥有一个看起来很棒的 App!
进一步阅读
-
docs.expo.dev/guides/icons/– Expo 图标指南. -
reactjs.org/docs/context.html– React 上下文。 -
github.com/expo/google-fonts– Expo Google Fonts.
第四章:为 Funbook 应用进行样式化和填充
在上一章中,我们根据设计文件规划了我们的应用所需的界面和组件。我们还熟悉了预览和调试应用的过程——至少我希望您已经熟悉了,亲爱的读者!无论您是使用 iPhone 或 Android 模拟器,还是使用带有 Expo Go 应用的实体设备,请确保您以这种方式检查您的应用。使用 Expo 构建的应用预览没有错误答案。在本章中,我们将对我们的界面和组件进行样式化。我们最终将看到一个看起来希望接近设计的应用!之后,我们将添加一些真实数据。
下面是我们计划在本章中实现的一些非常简短的列表:
-
创建和样式化组件
-
为应用拉取数据
到本章结束时,我们将拥有一个看起来不错的应用程序,它可以从外部 API 获取数据。您可以随时密切关注或编写自己的代码。
技术要求
为了跟随本章内容,您需要具备一些 JavaScript 和 ReactJS 的知识。如果您已经跟随了本书的前两章,您应该能够无任何问题地继续前进。
您可以自由选择您喜欢的 IDE,因为 React Native 不需要任何特定功能。目前,前端开发者中最受欢迎的 IDE 是微软的 Visual Studio Code、Atom、Sublime Text 和 WebStorm。
本章提供的代码片段旨在说明我们应该如何使用代码。它们并不提供完整的画面。为了更好地跟随代码编写过程,请在您的 IDE 中打开 GitHub 仓库并查看其中的文件。
如果您遇到困难或迷失方向,可以查看 GitHub 仓库中的代码:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-4。
创建和样式化组件
是时候创建一些真正的组件了!让我们从主页面开始。
我喜欢从上到下工作,所以我们首先从标题栏开始。我们的免费设计模板包括应用名称(“Socially”)和位于信息流顶部的铃铛图标。在我们的示例应用中,我们不会实现通知功能,所以我们将忽略设计文件中的这部分。标题栏的样式是通过 React Navigation 添加的。我们将向<Tab.Navigator>添加以下属性:
// …
headerTransparent: true,
headerTitleAlign: "left",
headerTitleStyle: {
paddingTop: 140,
paddingBottom: 40,
textAlign: "left",
fontWeight: "bold",
},
// …
正如我们在分析主页面前面所分析的,我们知道我们需要创建主页面的两个部分:一个头像列表和一个带有图片的卡片列表。头像列表将使用水平FlatList组件。列表上的第一个项目是不同的;它是一个用户用来添加内容的按钮。我们将向FlatList添加一个ListHeaderComponent属性,我们将在这里添加这个特殊项目。现在让我们创建一个占位符组件:
// src/components/ListHeaderComponent
import React from "react";
import { View, Text } from "react-native";
export const ListHeaderComponent = () => {
return (
<View>
<Text>List Header component placeholder</Text>
</View>
);
};
在前面的代码中,我们创建了一个名为 ListHeaderComponent 的组件,因此我们可以将其导入到 FlatList 中。到目前为止,此组件只显示占位文本。
我将设计文件中导出的几个个人头像图片添加到了 assets 文件夹中。我们将使用它们来构建头像列表。
这就是 ListOfAvatars 组件的样子:
// src/components/ListOfAvatars.js
import React from "react";
import { View, Text, FlatList} from "react-native";
import { ListHeaderComponent } from "./ListHeaderComponent";
const arrayOfAvatars = [
{
id: 1,
url: "",
},
{
id: 2,
url: "",
},
{
id: 3,
url: "",
},
];
export const ListOfAvatars = () => {
const renderItem = ({ item }) => {
return <Text>{item.id}</Text>
};
return (
<View style={{ paddingTop: 30 }}>
<FlatList
data={arrayOfAvatars}
renderItem={renderItem}
keyExtractor={(item) => item.id}
horizontal
ListHeaderComponent={<ListHeaderComponent />}
/>
</View>
);
};
请记住从 FlatList 中导入必要的组件。您可能还会注意到我设置了一个非常简单的数据数组,用于填充头像列表。我们将在稍后管理将此组件连接到我们的示例数据。
如果您更喜欢在屏幕上查看此代码而不是在书中,您始终可以在仓库中查看。我们目前正在工作的代码可以在 chapter-3 分支中找到:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-3。
一旦我们设置了 FlatList 并与实际图像建立了链接,我们就可以将 <Text> 组件更改为 <Image> 组件,从我们的数组中提供数据,添加一些样式使图像呈圆形,然后我们就完成了!
我们将添加一张卡片列表,它将与头像列表非常相似。我们还将使用一个临时数据的数组并添加一些样式,最终我们应该得到一个看起来像这样的组件:
// src/components/ListOfCards.js
export const ListOfCards = () => {
const renderItem = ({ item }) => {
return (
<Image
style={{
width: "100%",
height: 288,
borderRadius: 20,
marginBottom: 32,
}}
source={{
uri: item.url,
}}
/>
);
};
return (
<View style={{ paddingVertical: 30 }}>
<FlatList
data={arrayOfImages}
renderItem={renderItem}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
/>
</View>
);
};
被称为 Feed 的表面组件应该只关注导入正确的子组件和通用样式。它看起来是这样的:
// src/surfaces/Feed.js
export const Feed = () => {
const headerHeight = useHeaderHeight();
return (
<SafeAreaView
style={{ flex: 1, paddingTop: headerHeight + 20, paddingHorizontal: 20 }}
>
<View>
<ListOfAvatars />
<ListOfCards />
</View>
</SafeAreaView>
);
};
我们应用中的 Feed 表面应该看起来像这样:

图 4.1 – Feed 表面的 iPhone 模拟器截图
您可以保持您的应用原样,或者您可以复制我在 GitHub 仓库中添加的一些样式调整。在这本书中,我们不会专注于样式,所以我们将不会详细讨论它们;不过,我鼓励您四处看看。
Feed 表面看起来与设计非常相似,因此我们可以继续前进到 Conversations 表面。
我们的 <ConversationsNavigation>,我们将在这里创建一个 Stack Navigator:
// src/surfaces/ConversationsNavigation.js
import React from "react";
import { Conversations } from "./Conversations";
import { Messages } from "./Messages";
import { createStackNavigator } from "@react-navigation/stack";
const Stack = createStackNavigator();
export const ConversationsNavigation = () => {
return (
<Stack.Navigator
screenOptions={{
//…
}}
>
<Stack.Screen name='Conversations' component={Conversations} />
<Stack.Screen
name='Messages'
component={Messages}
options={({ route }) => ({
title: route.params.name,
//…
})}
/>
</Stack.Navigator>
);
};
在此组件中,我们设置的最有趣的选项是这一项:
options={({ route }) => ({
title: route.params.name,
//…
这行代码告诉 Messages 表面。如果您现在测试您的应用,您会注意到这还没有发生。我们还需要在用户选择进入 Messages 表面时设置此参数,这意味着我们需要在点击 Conversation 时设置它。我们将创建一个带有顶部输入框的 Conversations 表面,后面跟着 FlatList 中的对话列表。列表中的每个项目都将包裹在一个 <Pressable> 组件中,它看起来像这样:
<Pressable onPress={() => navigation.navigate("Messages", { name: item.name })} >
当我们的用户选择一个对话时,这个对话会将分配的name参数传递给Messages界面,界面随后将显示这个名称作为标题。现在我们可以添加一个假消息列表和条件样式,这些样式将根据消息是来自用户还是发送给用户而有所不同。对于消息列表来说,记住在消息的FlatList组件上使用inverted属性是个有用的技巧。毕竟,我们希望最新的条目出现在列表的底部。
你可能会注意到,此时Conversations界面和Messages界面都没有显示底部标签。实现这种功能最好的方法是,将我们的<ConversationsNavigation>从标签导航器移出,并移入主堆栈导航器。主堆栈中列出的界面将显示在标签导航器中的界面之上,此外我们还可以使用 React Navigation 库提供的预配置的返回按钮。以下是App.js根组件应该看起来像什么:
// src/App.js
export default function App() {
//…
return (
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator>
{!userLoggedIn ? (
<Stack.Screen name='Login' component={Login} />
) : (
<>
<Stack.Screen
name='Home'
component={Home}
options={{ headerShown: false }}
/>
<Stack.Screen
name='ConversationsNav'
component={ConversationsNavigation}
options={{ headerShown: false }}
/>
</>
)}
</Stack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
}
为了让我们的Conversations界面的按钮在标签中显示,我们需要创建一个空的虚拟界面并将其传递给 Tab Navigator:
// src/surfaces/Home.js
<Tab.Screen name='Feed' component={Feed} />
<Tab.Screen
name='ConversationsMain'
component={ConversationsBase} // just a dummy component which will never be called
options={{
tabBarIcon: ({ size }) => (
<Ionicons name='chatbox-outline' color='#000000' size={size} />
),
}}
listeners={({ navigation }) => ({
tabPress: (e) => {
e.preventDefault();
navigation.navigate("ConversationsNav");
},
})}
/>
<Tab.Screen name='AddPost' component={AddPost} />
// …
我们将在Conversations界面的底部添加一个浮动按钮,这样我们就完成了!
我会很快地浏览这些代码更改,因为我们不希望花太多时间关注样式或 React Navigation 技巧和窍门。我们希望尽快运行一个接近现实世界中的应用程序,这样我们就可以开始玩转状态和数据管理。请随意查看 GitHub 仓库中的所有代码更改,在那里你也可以提问和提出问题。
我们将继续通过向FlatList组件添加内容来推进我们的进度,这些组件将显示添加的图片和收藏的图片。
我们将通过向Tab Navigator的中央项目添加自定义组件来完成这个界面,这个黑色按钮用于添加帖子。我们可以添加任何我们想要的作为标签栏图标的自定义组件:
// src/surfaces/Home.js
function Home() {
return (
<Tab.Navigator>
//…
<Tab.Screen
name='AddPost'
component={AddPost}
options={{
tabBarIcon: ({ size }) => (
<View
style={{
marginTop: -30,
}}
>
<View
style={{
position: "absolute",
backgroundColor: "#000000",
padding: 30,
bottom: -10,
left: -13,
borderRadius: 23,
transform: [{ rotate: "-45deg" }],
shadowColor: "#000000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 4,
}}
/>
<Ionicons name='add-circle-outline' color='#ffffff' size={36} />
</View>
),
}}
/>
//…
如果你仔细观察,你会发现这个按钮的样式与Conversations界面上浮动的按钮非常相似。在我们需要重复大量代码的情况下,将其抽象到单独的文件中是个好主意。这被称为不要重复自己(DRY)编程。我们不想走得太远,为每一件小事都做抽象。还有一种编程原则叫做写两次(WET)编程,它主张编写冗长的代码,尤其是在开始一个新项目时。我个人的偏好是避免仓促抽象(AHA)编程,这是由Kent C. Dodds提出的。这种方法结合了 DRY 和 WET 原则,并鼓励我们程序员在不过度使用它们的同时,找到抽象的最佳用例。
在这个特定的情况下,我们正在重复样式。我们可以轻松创建一个名为floatingButton的类并将其应用于我们的两个组件。我们也可以使用<FloatingButton>样式组件。还有更多方法可以实现具有可重用样式的目标,但我们不会深入探讨。我将在我们的组件中进行一些清理,并在几分钟后在这里与你见面,这样我们就可以连接到我们的(几乎)真实 API 的一些真实数据。
拉取应用数据
欢迎回来!你有没有花点时间看看我们的应用代码?你是否从chapter-3分支克隆了仓库,或者你是否根据我之前描述的大致轮廓创建了你的组件?无论如何,我很高兴你在这里!让我们获取一些数据并使用一些状态!
关于我们将使用的数据的一个快速提醒:我在/docs文件夹中设置了 GitHub Pages,你可以在这里找到:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/docs。
你可以直接在 GitHub UI 中预览每个 JSON 文件。你还可以通过点击原始按钮查看任何文件的原始内容:

图 4.2 – GitHub UI,红色圆圈圈出了原始按钮
点击此按钮后可见的文本文件就是你可以看到的 API 响应。
我们将从获取用户列表开始。这个列表包含用户 ID 和用户头像的链接。我们的 API 依赖于我们在整个应用中管理用户头像,并且只通过这个端点传递它们。
那么,让我们检查我们需要用户列表的地方。我们需要它在App.js中。
我们首先要做的是在父组件中获取数据:
// src/App.js
export default function App() {
const [userLoggedIn, setIsUserLoggedIn] = useState(true);
const [userList, setUserList] = useState(null);
//…
async function fetchUserData(id) {
const response = await fetch(requestBase + "/users.json");
setUserList(await response.json());
}
useEffect(() => {
fetchUserData();
}, []);
//…
if (!userList) {
return <AppLoading />;
}
一旦我们获取了数据并将其放入userList对象中,我们就可以将其作为属性从父组件传递给子组件。根据 React Navigation 文档,你可以在导航器的渲染回调中传递额外的属性。以下是Home组件的示例:
<Stack.Screen name='Home' options={{ headerShown: false }}>
{(props) => <Home {...props} userList={userList} />}
</Stack.Screen>
一旦我们在Home界面中有了userList属性,我们就应该完成了,对吧?不幸的是,不是的。Home界面是标签导航器的父组件,因此我们需要为Feed界面添加整个渲染回调过程。一旦我们到达Feed界面,我们需要将userList属性传递给ListOfAvatars组件……这开始变得有点多了,不是吗?这是在大应用中被称为属性钻取的一个例子。通过多个界面和组件传递对象不仅繁琐,而且容易出错。这种设置很脆弱——只要链中的任何一个组件发生变化,整个应用可能就无法使用。我们能做些什么来避免这种情况呢?我们可以使用React Context。这也是 React Navigation 维护者推荐的战略。
什么是上下文?
上下文用于在组件树中传递数据,而无需手动将 props 传递到每个组件。
我们需要采取的第一个步骤是创建我们的上下文并设置初始值:
const UserListContext = React.createContext(null);
然后,我们需要将父组件包裹在一个带有更新值的 Context Provider 中:
// src/App.js
return (
<SafeAreaProvider>
<UserListContext.Provider value={{ userList: userList }}>
<NavigationContainer theme={MyTheme}>
<Stack.Navigator>
//…
上下文拼图的最后一部分是如何使用它,或者说“消费它”。一旦上下文提供给父组件,我们就可以通过<Context.Consumer>组件在其任何一个子组件中消费它。我们将把这个消费者添加到我们的头像列表中:
// src/components/ListOfAvatars.js
export const ListOfAvatars = () => {
const renderItem = ({ item }) => {
//…
};
return (
<UserListContext.Consumer>
{({ userList }) => (
<View
//…
>
<FlatList
data={userList}
renderItem={renderItem}
keyExtractor={(item) => item.id}
horizontal
//…
好了!我们成功获取了外部数据,将其提供给我们的应用,并借助 React Context 传递数据。使用上下文提供了更好的开发者体验;然而,它也带来了一系列问题。在使用上下文时,你应该始终牢记的最大问题是,上下文中的任何更改都会导致放置Provider的组件及其所有子组件的重新渲染。这意味着如果我们有一个 API,用户可以添加或删除他们列表中的其他用户,每次他们这样做时,整个应用都需要重新渲染。有时,我们希望这样;我们希望更新头像列表和Feed中的图片列表。我们也希望在这种情况下更新Conversations界面。但是关于Profile和Add Post界面呢?我们现在不会回答这些问题,因为我们正在处理一个示例应用。然而,每次你决定使用 React 的 Context 时,你应该问自己 Provider 应该放在哪里,以及当上下文的数据发生变化时会发生什么。
让我们继续为我们的应用的其他部分获取真实数据。我们希望在Feed上显示图片。我们将从使用useEffect钩子在ListOfCards组件中获取数据开始:
// src/components/ListOfCards.js
import AppLoading from "expo-app-loading";
import { requestBase } from "../utils/constants";
export const ListOfCards = () => {
const [cardList, setCardList] = useState(null);
async function fetchCardData() {
const response = await fetch(requestBase + "/home.json");
setCardList(await response.json());
}
useEffect(() => {
fetchCardData();
}, []);
if (!cardList) {
return <AppLoading />;
}
return (
//…
<FlatList
data={cardList.listOfitems}
renderItem={renderItem}
keyExtractor={(item) => item.itemId}
一旦我们的卡片项被获取并传递给Card组件,我们可以对它们做更多的事情——也就是说,我们可以将作者 ID 与上下文中的用户列表进行比对,并利用这些信息显示正确的用户名和头像。
我们将把与ListOfAvatars组件中添加的相同上下文消费者添加到Card组件中,但在这个情况下这还不够。一旦我们获取了整个列表,我们还需要找到与卡片作者 ID 匹配的用户 ID。我们将调整传递上下文值的方式,并过滤userList数组:
// src/components/Card.js
import { UserListContext } from "../context";
export const Card = ({ item }) => {
return (
<UserListContext.Consumer>
{({ userList }) => {
const currentUser = userList.filter(
(user) => user.id === item.authorId
);
return (
<View>
<Image
//…
我们创建了一个名为currentUser的变量,它是一个包含精确一个项目的数组——即发布特定卡片的用户。不幸的是,这个变量只能被Card组件访问。如果我们想在例如点击图片时打开的模态框中使用相同的信息,我们不得不将模态组件嵌套在Card组件中,或者再次搜索当前用户。您将在我们自己的应用中看到这个问题的例子,在我们接下来与Conversations界面工作的时候。
另一方面,我们使用Card组件组合了另一个表面——Favorites表面。我们只需获取Favorites数据即可使其正确工作。其余的都应该会自动就位。
如果你在从 GitHub 页面上的示例 API 加载数据时遇到任何问题,首先确保数据已被获取。您可以通过在代码中使用console.log并查看终端来检查对象是否已被获取。然后,您需要检查您是否正确拼写并嵌套了所有的名称和对象键。如果您在任何地方卡住了,请记住您始终可以前往 GitHub 上托管的项目,克隆它,并在您喜欢的任何阶段查看。
让我们继续到下一个需要获取数据的组件——Conversations。正如我之前提到的,我们需要调整并重复一些我们之前为Feed和Favorites表面中使用的Card组件编写的代码。在Conversations中,我们还将获取用户列表并搜索当前用户。请确保一切拼写正确。那个偷偷摸摸的 API 作者把一切都命名为不同的名称!以下是我的Conversations组件的样子:
// src/components/ConversationItem.js
export const ConversationItem = ({ navigation, item }) => {
return (
<UserListContext.Consumer>
{({ userList }) => {
const currentUser = userList.filter((user) => user.id === item.userId);
return (
<Pressable
onPress={() =>
navigation.navigate("Messages", {
name: currentUser[0].name,
avatar: currentUser[0].url,
})
}
style={{
height: 103,
//…
请注意currentUser后面的[0]。我们在一个数组上使用了一个过滤器函数,结果是一个数组。省略[0]意味着应用程序将不会显示任何数据,因为它会看到一个数组而不是一个对象。
我们已经有了会话列表;现在是时候在用户点击时获取特定的会话了。重定向到消息屏幕的动作发生在Conversations表面的FlatList中的<ConversationItem>组件上。Messages表面是同一个栈导航器的一部分,这意味着我们可以从这里选择两个方向:
-
向
<ConversationsNavigation>组件添加上下文,在会话被点击时设置其值,并在Messaging表面消费它。 -
将会话 ID 作为路由参数传递,同时传递用户数据。
第二种方法非常有吸引力,因为它很简单。我们只是添加了一块我们已经有权限访问的数据,并通过导航传递到正确的位置。这种方法的本质并没有什么可以立即批评的地方。然而,在现实世界的应用程序中,您可能会编写非常大的或重复的对象,并将它们作为route参数传递。根据 React Navigation 文档,尽管使用路由参数很方便,但它们不应作为全局应用程序状态的替代品。手动通过路由参数传递数据可能会导致错误,并使应用程序显示过时的数据。如果您想稍微锻炼一下,您现在就可以在自己的FunBook应用程序副本中实现这个解决方案。
当你准备好了,请回到这里,我会带你一步步创建和消费用于会话的新上下文。
我们将像以前一样开始,创建具有初始值的上下文:
export const ConversationContext = React.createContext(null);
我决定将创建上下文的函数放在一个单独的文件中,为了简单起见,我们将其命名为context.js。一旦上下文被创建,我们需要将其包裹在正确的组件周围。在这种情况下,我们需要在Conversations表面的嵌套Stack``Navigator周围添加 Provider。让我们在<ConversationsNavigation>组件中添加以下代码:
// src/surfaces/ConversationsNavigation.js
import { ConversationContext } from "../context";
//…
export const ConversationsNavigation = () => {
const [conversationId, setConversationId] = useState(null);
return (
<ConversationContext.Provider
value={{
conversationId: conversationId,
setConversationId: setConversationId,
}}
>
<Stack.Navigator
screenOptions={{
headerBackTitleVisible: false,
// …
你会注意到这次我们传递了值和setter函数到上下文中。这是因为我们将在树中更深的地方,在<ConversationItem>组件中设置上下文的值。不过不用担心;通过上下文传递函数是完全没问题!
当你注意到<ConversationItem>已经被<UserListContext.Consumer>包裹时,你可能会问关于多个上下文的问题。这完全没问题。你可以拥有你需要的任何数量的包装器!以下是我们的具有两个上下文的组件的样子:
// src/components/ConversationItem.js
export const ConversationItem = ({ navigation, item }) => {
const onPressItem = (setConversationId, currentUser) => {
setConversationId(item.id);
navigation.navigate("Messages", {
name: currentUser[0].name,
avatar: currentUser[0].url,
});
};
return (
<ConversationContext.Consumer>
{({ setConversationId }) => (
<UserListContext.Consumer>
{({ userList }) => {
const currentUser = userList.filter(
(user) => user.id === item.userId
);
return (
<Pressable
onPress={() => onPressItem(setConversationId, currentUser)}
//…
既然我们已经设置了上下文,让我们在Messages表面消费它。我们需要首先从上下文中获取对话 ID,然后获取给定对话的正确JSON文件。我们将在Messages表面添加<ConversationContext.Consumer>作为包装器:
// src/surfaces/Messages.js
export const Messages = ({ route }) => {
const headerHeight = useHeaderHeight();
return (
<SafeAreaView style={{ flex: 1, paddingTop: headerHeight + 100 }}>
<ConversationContext.Consumer>
{({ conversationId }) => (
一旦我们获取到对话 ID,我们将在ListOfMessages组件中使用它来获取与给定屏幕相关的数据:
// src/components/ListOfMessages.js
import AppLoading from "expo-app-loading";
import { requestBase } from "../utils/constants";
export const ListOfMessages = ({ conversationId }) => {
const [messages, setMessages] = useState(null);
async function fetchMessages() {
const response = await fetch(
requestBase + "/messages/" + conversationId + ".json"
);
setMessages(await response.json());
}
useEffect(() => {
fetchMessages();
}, []);
if (!messages) {
return <AppLoading />;
}
const renderItem = ({ item }) => {
//…
};
return (
//…
<FlatList
data={messages.messages}
renderItem={renderItem}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
inverted
/>
</View>
//…
好的,我们做到了!在这里我们做了一些扎实的工作;现在是时候给自己鼓掌了。我们设置了多个组件来获取数据并在必要时传递它。我们已经设置了Feed组件、Favorites、Conversations和Messaging。最后剩下的表面是Profile。我将把它留给你,亲爱的读者,来管理这个表面的数据。我相信你已经在本章中学到了足够多的知识,能够独立完成它。
当你访问书籍仓库时,你将在名为chapter-3的分支上找到与本章节相关的所有工作。你可以浏览提交记录来查看应用开发的进展,或者你可以简单地查看应用的最终状态。在下一章中,我们将看到我们是否可以用一个更全局的解决方案 Redux 来替换所有的上下文、props 和过滤用户。继续前进,不断进步!
摘要
我们在本章中做了很多出色的工作!当你看到一款看起来很棒且运行流畅的应用时,你会有一种非常特别的满足感,对吧?
在这一章之后,我们达到了这样的境地——我们有一个按照设计风格定制的应用。这个应用从 API 中拉取外部数据。我承认我们的应用相当简单。还有很多更多功能可以添加到社交媒体克隆应用中。没有任何东西阻止你这样做。你可以随意添加和删除任何你想要的东西。我还会添加一些更多功能,也许是一个模态框,或者一个功能性的“点赞”按钮,我们将在第五章中见面,在那里我们将开始调查我们的第一个状态管理解决方案——Redux。
进一步阅读
-
www.digitalocean.com/community/tutorials/what-is-dry-development: DRY 编程。 -
betterprogramming.pub/when-dry-doesnt-work-go-wet-6befda0444bf: WET 编程。 -
kentcdodds.com/blog/aha-programming: AHA 编程。 -
reactnavigation.org/docs/hello-react-navigation/#passing-additional-props: 在 React Navigation 中传递额外的 props。 -
reactjs.org/docs/context.html: React Context。 -
reactnavigation.org/docs/params/#what-should-be-in-params: React Navigation – params 中应该包含什么?
第三部分 – 探索 React Native 中各种状态管理库
在这部分,我们将从 Redux 和其工具包开始;我们将学习它们为什么被创建,如何配置它们,以及如何在示例应用中用它们来管理点赞的图片。接下来,我们将学习关于 MobX 的内容,它想要解决什么问题,以及如何配置和使用它来在 Funbook 应用中管理点赞的图片。然后,我们将学习关于 XState 的内容,这个库的数学基础是什么,如何配置它,以及如何利用其可视化器来可视化数据。最后,我们将将其实现用于在 Funbook 应用中管理点赞的图片。接下来是 Jotai;我们将了解它为什么被创建以及它解决了什么问题。然后,我们将为 Funbook 应用配置它并使用它来管理点赞的图片。最后,我们将学习关于 React Query(或 TanStack Query)。我们将了解为什么这个库在一本关于状态管理的书中被提及。然后,我们将配置它并用于在 Funbook 应用中获取点赞的图片。
本部分包括以下章节:
-
第五章,在我们的 Funbook 应用中实现 Redux
-
第六章,在 React Native 应用中使用 MobX 作为状态管理器
-
第七章,使用 XState 在 React Native 应用中解开复杂流程
-
第九章, 使用 React Query 进行服务器端驱动状态管理
第五章:在我们的 Funbook 应用中实现 Redux
在上一章中,我们稍微“沾染”了一些实际操作。我希望你喜欢构建 Funbook 应用!我们成功构建了一个功能应用的客户端。当然,我们创建的功能是有限的。一个现实世界的社交媒体应用将更加健壮,拥有更多的组件和用户流程。然而,更大的应用也会带来自己的一套问题:处理大量数据集、建立风格指南、管理分析以及许多其他我们不希望花费时间解决的问题。我们在这里讨论不同状态管理的解决方案。为了保持专注,我在应用中添加了一些在上一章中没有详细描述的功能。我添加了一个模态,显示 GitHub 上example-app-full文件夹中图像的放大版本:
github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/example-app-full.
这个应用将成为我们在这本书中所有状态管理实验的基础。我们将从查看最古老的状态管理库:Redux开始。
在本章中,我们将进行以下操作:
-
简要回顾一下Redux的历史
-
在 Funbook 应用中安装和配置Redux
-
向应用添加Redux功能
-
了解如何调试Redux
到本章结束时,你应该能够熟练使用Redux特定的术语,如 reducer、actions 和 store。你也应该对在真实的React Native应用中配置和使用Redux有很好的理解。
技术要求
为了跟上本章的内容,你需要具备一些 JavaScript 和 ReactJS 的知识。如果你已经跟随着本书的前两章,你应该能够没有问题地继续前进。
随意使用你选择的 IDE,因为 React Native 不需要任何特定功能。目前,前端开发者中最受欢迎的 IDE 是微软的 VSCode、Atom、Sublime Text 和 WebStorm。
本章提供的代码片段旨在说明我们应该如何使用代码。它们并不提供完整的画面。为了更容易地编码,请在你选择的 IDE 中打开 GitHub 仓库,查看其中的文件。你可以从名为example-app-full或chapter-5的文件夹中的文件开始。如果你从example-app-full开始,你将负责实现本章中描述的解决方案。如果你选择查看chapter-5,你将看到我实现的整个解决方案。
如果你遇到了困难或迷失了方向,你可以在 GitHub 仓库中查看代码:
github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-5.
什么是 Redux?简史
我们在 第一章 中简要介绍了 React 的历史,什么是 React 和 React Native? 如果你跳过了那一章,或者只是不记得了,不用担心。你需要知道的是,ReactJS 于 2013 年发布,它为创建美观的单页应用打开了大门。ReactJS 是一个令人兴奋的库!很多人抓住了这个机会,开始重新编写他们的网站。随着时间的推移,许多开发者会发现,使用 ReactJS 创建和维护大型应用变得乏味。别忘了,在 ReactJS 团队引入 hooks 和 context 之前,这种情况就已经发生了。开发者必须从父组件传递 props 到嵌套子组件,经过多个无关组件的层级。这被称为 prop 钻孔,因为通过许多祖先到达子组件的感觉就像是在钻孔。
2015 年,发生了一件非常有趣的事情:丹·阿布拉莫夫 和 安德鲁·克拉克 编写并发布了一个名为 Redux 的新开源库。最初,ReactJS 开发者对此感到困惑,因为 Redux 为 ReactJS 世界引入了新的概念。我们可以开始思考那些可以从应用中的任何地方访问的全局状态。为了改变全局状态,我们需要使用称为“actions”的特殊函数,还需要使用称为“reducers”的东西... 这需要吸收很多内容!不管怎样,这个新库解决了一个非常实际的问题,所以唯一要做的就是系好安全带,观看丹·阿布拉莫夫的教程,并使用这个新颖而神奇的工具!
多亏了丹·阿布拉莫夫(Dan Abramov)在教学、解释和推广 Redux 方面的努力,它成为了 ReactJS 开发的基础。随着岁月的流逝,为管理全局状态而创造的新概念不断涌现,有些与 Redux 类似,有些则非常不同。与较新的解决方案相比,Redux 可能会显得有些笨拙,因为它有大量的样板代码。即使是库的作者也通过推特表达了他的疑虑:

图 5.1 – 丹·阿布拉莫夫(Dan Abramov)的推文,表示他不理解 Redux 示例代码
大约在 2016 年,Redux 的维护工作转交给了 马克·埃里克森 和 蒂姆·多尔。我有机会和马克·埃里克森交换了几条信息。他告诉我,他并没有因为维护 Redux 而获得报酬;他是在业余时间做的,尽管这可能会非常耗时。他自己也表示,他成为 Redux 的维护者是个意外,但在我阅读了他关于这个主题的优秀博客文章后,我认为他成为 Redux 的维护者是因为他在 Redux 文档上投入了大量的工作,以及他花费时间帮助使用 Redux 的开发者。你可以在他的博客上阅读完整的故事(进一步阅读 部分中的链接)。马克补充说,他喜欢维护 Redux。他有时会与对他在做决策时感到不满意的开发者发生冲突,但他也收到了来自同行 OSS 维护者以及会议邀请的支持。我询问马克他对 Redux 在当前状态管理库领域的看法。他指出,有许多资源(NPM 统计数据、GitHub 统计数据等)证明 Redux 仍然是迄今为止在 React 应用中最广泛使用的状态管理库。然而,正如马克所说,从 2016 年到 2017 年,Redux 被过度使用了。在那段时间里,许多开发者提出了关于 Redux 模板大小合理的投诉。这种情况反过来又导致了 Twitter 上的反弹,很多人声称“Redux 已死”,因为某个工具或另一个“杀死了它”。
“RTK 和 React-Redux 钩子改变了这一说法。如果你看看今天的 Reddit 和 Twitter 上的讨论,你会看到很多人表示他们非常喜欢 RTK 并推荐它,”马克说。
Redux 目前是管理 React 和 React Native 应用程序全局状态的一个成熟且值得信赖的解决方案。我们在这个部分简要地回顾了其历史。很明显,它有其不足之处。引用马克·埃里克森的话,“这是一个有用的工具,并不适用于所有情况,但是一个非常合理的选择。”它有它的支持者和反对者,但了解它是有价值的——这就是我们在这里的原因!让我们开始吧!
安装和配置 Redux
就像我们想要添加到项目中的任何库一样,我们首先会阅读文档。Redux 的文档在多年中已经发生了很大的变化。到 2022 年,推荐的安装包括 Redux Toolkit。
Redux Toolkit 是使用 Redux 的官方推荐方法。它包含构建 Redux 应用程序常用的包和依赖项。这个工具包还简化了许多使用 Redux 必须完成的任务,例如创建存储或减少器。任何用户都可以自由安装和使用核心 Redux,但我们将使用推荐的方法并使用 Redux Toolkit。
为什么不直接使用 Redux?
Redux 库自 2015 年构思以来已经发生了很大的变化。其生态系统也增长了很多。2022 年推荐的 Redux Toolkit 是为用 Redux 编写的应用程序最实用的补充,尽管它不是必需的。
让我们从完整应用程序的文件开始,这些文件位于 example-app-full 文件夹中。您可以直接在您的计算机上修改这些文件。您也可以分叉存储库或从该文件夹复制文件。这些文件包括运行完整应用程序所需的所有内容。如果您想跟随工作代码,您应该查看 chapter-5 文件夹。那里放置了本章所有完成的工作。
让我们开始吧。按照以下步骤操作:
-
一旦您进入
app文件夹,请运行以下命令:npm install @reduxjs/toolkit
我们将继续安装 Redux 文档中推荐的补充包。
-
让我们运行以下命令:
npm install react-reduxnpm install --save-dev @redux-devtools/core
现在依赖项已安装,我们可以花一分钟时间讨论 Redux 的核心概念。
主要概念,也是绝对最重要的一个,就是我们在 Redux 中将状态视为一个普通对象。Redux 文档使用待办事项应用作为示例,但我们可以继续使用我们的 Funbook 应用程序。
如果我们要用一个单独的对象来表示 Funbook 应用程序的登录用户的状态,它可能看起来像这样:
{
userLoggedIn: true,
userData: {
id: 3,
name: "John Doe",
email: "john@doe.com",
image: "imageURL",
addedImages: […],
likedImages: […],
numberOfPosts: 35,
numberOfFollowers: 1552,
numberOfFollows: 128,
idsOfFollowedUsers: […],
idsOfConversations: […]
},
}
在这个例子中,我们试图全面了解整个应用程序所需的所有用户数据。这被认为是全局状态。我们不是面向表面;我们想知道与用户相关的所有数据。因此,在 userData 对象中,您将找到用于 Profile 界面的用户名和电子邮件,我们可以使用 Feed 界面中的用户头像的跟随用户 ID 数组,以及用于 Conversations 界面的必要对话 ID 数组。
当然,我们应用程序的所有数据并不直接依赖于登录用户。让我们尝试想象 Feed 界面上出现的模态的全局状态部分的形状。以下是图像点击打开的模态的状态可能看起来像这样:
{
imageModalOpen: true,
imageId: 3,
authorId: 3,
imageUrl: "imageUrl",
numberOfLikes: 28,
numberOfConversations: 12,
numberOfFollows: 128
}
在应用程序周围走一圈,我们可能想要考虑与 Conversations 界面相关的全局状态切片的形状。在我看来,我们从 GitHub Pages 上设置的模拟 API 中获取的数据形状与全局状态的形状非常吻合:
[
{
"id": 1,
"userId": 2,
"text": "Hey, how's it going?"
},
{
"id": 2,
"userId": 4,
"text": "Yo, are you going to the wedding?"
},
//…
通常情况下,全局状态与 API 响应具有相同的形状是受欢迎的。在这些情况下,作为前端开发人员,您将不必重塑数据或记住在何处以及为什么使用哪些键。在一个完美的世界里,API 响应将始终适合显示在 UI 上的数据形状。然而,在现实世界中,这可能意味着前端会不必要地获取可以在多个界面之间共享的数据,或者获取不必要的大数据集或图像。
我觉得我们正在掌握全局状态这个整体概念,对吧?请随意尝试自己思考应用可能需要的其他全局状态片段。也许你可以勾勒出当点击头像时显示的模态所需的全球状态形状——或者也许是什么具体需求对于“收藏”图像的界面,以及“个人资料”界面上的相同数据。当你准备好继续学习第二个 Redux 概念:分发动作时,请回到这里。
哦,你好!你回来了!太好了!那么,让我们更深入地讨论 Redux 吧!
分发动作
假设我们已经设置了全局状态——我们替换了很多不必要的属性,我们很满意——但如果我们想改变某些内容呢?如果用户喜欢一张图片呢?如果用户添加了一张新图片或关注了另一个用户呢?我们需要告诉我们的状态,某些内容已经改变。这就是我们分发动作的时候。一个动作是一个描述正在发生什么的普通 JavaScript 对象。我们可以分发一个类似这样的动作:
{ type: 'LIKE_IMAGE', payload: { Object with data about the liked image } }
现在怎么办?全局状态神奇地改变了?不幸的是,并没有。我们仍然需要告诉 Redux 根据这个动作来改变状态。将动作与状态联系起来的这个谜题的缺失部分被称为 reducer。Reducer 函数是接收旧状态和动作并返回应用新状态的普通 JavaScript 函数。以下是一个非常简单的用于收藏图片的 reducer 示例:
function likedImages(state =[], action) {
if (action.type === 'LIKE_IMAGE') {
let newLikedImages = state;
newLikedImages.push(action.payload);
return newLikedImages
} else {
return state
}
}
我们接收旧状态——在这种情况下,收藏图片的数组。然后我们添加新项目并返回新状态。我们还在 else 块中获得了非常优雅的错误处理,如果在任何地方出现问题,应用将返回到旧状态。
我在本节中描述了三个概念:
-
存储 – 全局状态的唯一真相来源
-
Reducer – 接收旧状态和动作的函数,执行所需操作,并返回新状态
-
Action – 包含存储所需信息的普通 JavaScript 对象
这些基本上就是您需要了解的,以有效地开始使用 Redux。如果您想了解更多关于这些概念和这个伟大库的历史,请查看 进一步阅读 部分,在那里您可以找到 Redux 文档的链接。现在我们已经了解了基础知识,我们准备将这项新知识应用到实际应用中。
将 Redux 功能添加到应用中
在上一节中,我们已经使用友好的包管理器安装了 Redux Toolkit,但我们在应用中还没有做出任何真正的改变。然而,我们在前面的章节中已经思考了应用中的数据流。我们现在需要做的工作将与之前非常相似。我们将从设计状态结构和动作开始。当我们有了这两者,我们将添加 reducer 来将一切联系起来。
我们面前还有大量的工作要做,所以让我们尝试将其分解成更小的块。我们将从查看用户状态以及如何在Redux中使用全局状态管理用户的登录和注销状态开始。然后我们将对应用中的喜欢图片做同样的遍历。当我们成功设置这两个全局状态的片段后,我们将看看如何将它们结合起来并在我们的应用中使用它们。然后我们将创建一些处理应用事件的 actions。一旦我们有了状态和 actions,我们将简要地看看如何在Redux应用中获取数据。最后,我们将准备好丢弃之前用于管理我们应用状态的React上下文。
用户登录状态遍历
让我们从用户状态开始。我们将创建一个名为store.js的新文件,我们将在这个文件中存储我们的初始状态片段。我们将向该文件添加以下JavaScript对象:
export const user = {
userLoggedIn: false,
userData: null,
};
当应用首次加载时,我们将假设用户未登录且没有用户数据。
现在,我们需要考虑一个当用户登录时将被分发的 action。它应该看起来像这样:
{type: 'LOGIN', payload: userData}
最后的部分是 reducer。让我们为我们的 reducers 创建一个新的文件夹,叫做……好吧,reducers。在这个文件夹内,我们将创建我们的 reducer 文件,它应该看起来像这样:
// reducers/user.js
import { user } from "../store";
export const login = (state=user, action) => {
if (action.type === 'LOGIN') {
return {
...state,
user: {
userLoggedIn: true,
user: action.payload,
},
}
} else {
return state
}
}
我们将用户对象作为初始状态导入,然后添加一个 switch,它会监听特定的 action。让我们监听'LOGIN' action。
但是等等——如果我们的用户想要注销呢?我们需要为这个特定的操作创建另一个 action:
{ type: 'LOGOUT' }
在这个情况下,我没有添加任何 action payload,因为我们不会传递任何实际的数据。我们只想清除数据,我们将在 reducer 中这样做。我们可以在 reducer 中添加另一个if语句,但大的if-else语句变得难以阅读和推理。对于 reducers 来说,使用switch语句是个好主意,因为我们实际上是在切换应用的不同状态。这就是我们的 reducer 将看起来像这样:
export const login = (state=user, action) => {
switch (action.type) {
case "LOGIN": {
return {
...state,
user: {
userLoggedIn: true,
user: action.payload,
},
};
}
case "LOGOUT": {
return {
...state,
user: {
userLoggedIn: false,
user: null,
},
};
}
default:
return state;
}
}
好的——现在当用户登录时,我们将设置应用的全局状态以反映这一点,对吧?几乎是这样!我们仍然需要找到我们代码中正确的位置来分发这个 action,而这个位置是登录界面的登录按钮——但是我们的登录界面是基于主组件的本地状态显示的!这意味着在我们看到 Redux 的魔法之前,我们还需要做一点额外的工作。不过别担心,这会值得的!
重要信息
如果你对我们似乎在做的大量额外工作有任何疑问,我邀请你,我亲爱的读者,去阅读一下useReducer钩子。在这个时候,useReducer听起来可能很熟悉,那是因为它是一个具有与Redux reducers 相同功能的ReactJS钩子。我希望到现在你已经开始相信,使用像Redux这样的状态管理库是 React Native 应用的绝佳解决方案。
你可能想知道为什么我们在状态中使用扩展运算符然后改变了 userLoggedIn 的值。理论上,直接在状态中更改值不更简单吗?不是在 Redux 中。Redux 非常坚决地认为减少器不能修改当前状态。减少器只能复制状态并对复制的值进行更改。这很重要,这样我们的代码才是可预测的。如果有许多减少器更改了相同的状态片段,谁能说结果会是什么?
不可变性
这是一个非常复杂的词,不是吗?它的意思是指某物不可更改,或者不应该更改。在 JavaScript 应用程序的情况下,不可变数据管理可以提高性能并使编程和调试更容易。Redux 减少器接受旧状态和动作,并返回一个新的状态对象;它们永远不应该对“旧”状态对象应用更改。
如果你好奇 Redux 的关键概念,我再次邀请你到 进一步阅读 部分,在那里你可以找到一个链接到由 Redux 的作者 Dan Abramov 创建的免费课程 Egghead.io。
使用 Redux 处理点赞的图片
我们的全局状态到目前为止相当薄弱。将用户数据保留在全局状态中是很好的,但我们当然可以用这个伟大的工具做更多的事情。比如点赞帖子?点赞帖子的减少器看起来像这样:
export const likedImages = (state = [], action) => {
if (action.type === "LIKE_IMAGE") {
let newLikedImages = state;
newLikedImages.push(action.payload);
return newLikedImages;
} else {
return state;
}
};
如果用户决定取消点赞一个帖子呢?让我们为这种情况添加一个动作和一个减少器:
{ type: 'UNLIKE_IMAGE', payload: { Object with data about the unliked image } }
现在,让我们调整我们的减少器。由于我们在单个减少器中有多个动作,我们将再次使用 switch 语句:
// ./reducers/likedImages.js
export const likedImagesReducer = (state = [], action) => {
switch (action.type) {
case "LIKE_IMAGE": {
const newLikedImage = action.payload;
return [...state, newLikedImage];
}
case "UNLIKE_IMAGE": {
const stateWithoutLikedImage = state.filter(
(item) => item !== action.payload
);
return stateWithoutLikedImage;
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
};
结合各种全局状态
我们有两个减少器,每个减少器都旨在管理两个不同的动作。我们现在需要做的是创建一个表示 Funbook 应用程序全局状态的存储库并将动作传递给减少器。我们可以使用来自核心 configureStore 函数的 createStore 函数,这将为我们做很多繁重的工作。我们只需要添加这个函数:
// ./store.js
import { configureStore } from "@reduxjs/toolkit";
import usersReducer from "./reducers/users";
import likedImagesReducer from "./reducers/likedImages";
export const store = configureStore({
reducer: {
user: usersReducer,
likedImages: likedImagesReducer,
},
});
configureStore 函数为我们合并了两个减少器,创建了 Redux 所需的根减少器。这个单一的根减少器是实现应用程序中单一事实来源所必需的。此函数还添加了一些有用的中间件功能,这些功能将检查常见错误并使我们的代码更容易调试。
我们创建了全局状态,并通过 <Provider> 组件包装器配置了它(无意中开玩笑),这个命名约定不是偶然的。两个 <Provider> 组件都服务于相同的目的,React 上下文使用了与 Redux 相同的大量高级逻辑。
让我们将必要的元素导入我们的主应用文件,App.js:
import { store } from "./store";
import { Provider } from "react-redux";
让我们将我们的应用包裹在 Redux <Provider> 中:
export default function App() {
//…
return (
<SafeAreaProvider>
<Provider store={store}>
//…
这看起来很熟悉,不是吗?<Provider> 与 React 的上下文有很多相似之处。我无法提供任何来自 Meta 团队的官方博客文章链接,React 维护者正式解释了这一点。然而,我可以提供我的个人观点,即 React 团队看到了 Redux 为大型 React 应用带来的解决方案,并认为其中的一些原则值得引入到 React 仓库本身。显然,还有其他的状态管理解决方案。如果没有,我就无法写这本书!无论如何,Redux 在 React 生态系统中占据着一个特殊的位置。
在短暂的休息之后,我们将重新回到我们的代码!我们已经设置了我们的存储和 Provider。我们还准备好了两个减法器:用于用户数据和喜欢的图像数据。让我们从替换喜欢的图像开始。我们将进入 surfaces 文件夹,在那里我们将找到 Favorited 表面。这将反过来引导我们到名为 ListOfFavorites 的组件,该组件显示来自 Favorited 上下文的数据。
我们将移除这个上下文,并使用 Redux 的 useSelector 钩子,然后我们将使用这个钩子从 Redux 获取实际数据:
// src/components/ListOfFavorites
import { useSelector } from "react-redux";
export const ListOfFavorites = ({ navigation }) => {
const { likedImages } = useSelector((state) => state.likedImages);
//…
你在我们的手机或模拟器上运行我们的应用了吗?我希望你已经运行了,因为这样你就会注意到刚刚发生了一些非常错误的事情!

图 5.2 – 带有 Redux 错误的 iPhone 模拟器截图
likedImages 减法器中的 switch 语句!这并不是我们想要的默认值,所以让我们继续修改,让它默认返回初始状态:
//reducers/likedImages.js
export const likedImagesReducer = (state = [], action) => {
switch (action.type) {
//…
default: {
return state;
}
}
};
应用程序正确加载 – 我们又重新开始工作了!我们将初始状态作为默认值传递给 likedImages 减法器,这意味着我们传递了一个空数组 – 但我们想要获取图像数据。我们之前在上下文提供者中使用 fetch 做过这件事。FavoritedContextProvided 使用了 React 的 useReducer 钩子,以及当图像成功获取时发出的 init_likes 动作。当涉及到 Provider 时,我们将在一个动作中创建一个获取函数,然后当 Favorited 表面被渲染时,我们将发出这个动作。这是一个针对简单应用的简单解决方案。如果你正在开发一个更大的应用,你可能需要关注缓存、避免重复请求或缓存有效期。在这种情况下,你应该查看 Redux Toolkit 提供的工具,称为 RTK 查询,它简化了 Redux 应用中的数据获取和缓存。
一个完整的工具包
同时学习这么多工具可能会开始感到有些压倒性。我们开始使用 Redux,然后继续使用 Redux Toolkit,现在我们正在添加 RTK Query。在这个阶段,不必太担心库和工具的名称。我们在这里是为了学习如何有效地使用 Redux 管理状态来编写应用程序,并且我们正在遵循文档和最佳实践来做这件事。一旦你熟悉了建议的解决方案,你可以自由地探索 Redux 生态系统,找到你最喜欢的方法。关于你喜欢和不喜欢的东西,没有错误答案!
利用 Redux Toolkit 创建动作
我们的红利器到目前为止非常有限。我们不能直接用它来获取数据,因为根据 reducer 状态规则,reducer 不能用于执行任何异步逻辑。如果我们是在 2018 年或 2019 年左右编写应用程序,我们可能会创建一个单独的actions文件,手动配置createSlice。在likedImagesreducer 中,“slice”是一个Redux Toolkit切片:
//reducers/likedImages.js
import { createSlice } from "@reduxjs/toolkit";
export const likedImagesSlice = createSlice({
name: "likedImages",
initialState: [],
reducers: {
likeImage: (state) => {
const newLikedImage = action.payload;
return [...state, newLikedImage];
},
unLikeImage: (state, action) => {
const stateWithoutLikedImage = state.filter(
(item) => item !== action.payload
);
return stateWithoutLikedImage;
},
},
});
export const { init, likeImage, unLikeImage } = likedImagesSlice.actions;
export default likedImagesSlice.reducer;
获取数据
由于Redux Toolkit中的createAsyncThunk函数。
什么是 thunk?
thunk 是一种特殊的函数,由另一个函数返回。这个名字与Redux本身无关。
这就是我们的获取数据 thunk 将看起来像:
import { createAsyncThunk } from "@reduxjs/toolkit";
import { requestBase } from "./src/utils/constants";
export const fetchLikedImages = createAsyncThunk(
"likedImages/initLikedImages",
async () => {
const response = await fetch(requestBase + "/john_doe/ likedImages.json");
return await response.json();
}
);
现在,我们需要告诉由Redux Toolkit提供的extraReducers函数,以保持我们的 reducer 整洁和可读:
// reducers/likedImages.js
import { createSlice } from "@reduxjs/toolkit";
import { fetchLikedImages } from "../asyncFetches";
export const likedImagesSlice = createSlice({
name: "likedImages",
initialState: {
likedImages: [],
loading: true,
},
reducers: {
//…
},
extraReducers: (builder) => {
builder.addCase(fetchLikedImages.pending, (state) => {
state.loading = true;
});
builder.addCase(fetchLikedImages.fulfilled, (state, action) => {
state.likedImages = action.payload;
state.loading = false;
});
builder.addCase(fetchLikedImages.rejected, (state) => {
state.loading = false;
});
},
});
现在我们已经有了一种相当优雅的方式来管理获取数据,包括挂起状态和拒绝状态,让我们实际获取我们的数据。我们不应该在ListOfFavorited组件中获取它,因为我们需要在整个应用程序渲染后立即获取图像数据。我们应该在父组件Home中获取图像:
//src/surfaces/Home
import { fetchLikedImages } from "../../asyncFetches";
import { useDispatch, useEffect } from "react-redux";
// …
export const Home = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchLikedImages());
}, []);
这样,当应用程序渲染并且用户在ListOfFavorites组件上时,将获取喜欢的图像数据:
//src/components/ListOfFavorites
import { useSelector, useDispatch } from "react-redux";
export const ListOfFavorites = ({ navigation }) => {
const { likedImages } = useSelector((state) => state.likedImages);
const dispatch = useDispatch();
const [imageList, setImageList] = useState([]);
useEffect(() => {
const reversedImages = [...likedImages].reverse();
setImageList(reversedImages);
}, [likedImages]);
if (!imageList) {
return <AppLoading />;
}
//…
<FlatList
data={imageList}
renderItem={renderItem}
keyExtractor={(item) => item.itemId}
//…
你可能已经注意到了获取的数据是如何传递给状态钩子的:
const reversedImages = [...likedImages].reverse();
我们使用 ES6 扩展运算符来将reverse()函数应用于likedImages数组的副本。这是因为likedImages数组是只读的,我们无法直接对其操作。
替换上下文
抽取一点时间来审视你所取得的成就。你有效地用 Redux 替换了 Favorited 上下文!我们最后需要做的就是替换当图像被喜欢或不喜欢时的动作,然后我们就可以做一些清理工作了!
让我们进入ImageDetailsModal界面,并将与上下文相关的代码替换为 Redux 代码:
//src/surfaces/ImageDetailsModal
import { likeImage, unLikeImage } from "../../reducers/ likedImages";
import { useDispatch, useSelector } from "react-redux";
export const ImageDetailsModal = ({ navigation, route }) => {
const { likedImages } = useSelector((state) => state.likedImages);
const [isCurrentImageLiked, setIsCurrentImageLiked] = useState(false);
const dispatch = useDispatch();
useEffect(() => {
const checkIfLiked =
likedImages?.filter(
(favoritedImg) => favoritedImg.itemId === route.params.imageItem.itemId
).length > 0;
setIsCurrentImageLiked(checkIfLiked);
}, [likedImages]);
我们最后需要更改的是当点击喜欢按钮时调用的函数:
<Pressable
onPress={() => {
if (isCurrentImageLiked) {
dispatch(unLikeImage(route.params.imageItem));
} else {
dispatch(likeImage(route.params.imageItem));
}
}}
>
我们已经完成了应用Favorited上下文Provider。
我们的应用程序仅由功能组件组成,因此我们可以使用mapStateToProps和mapDispatchToProps。尽管如此,现代React应用程序可以不使用类组件来构建——正如你在 Funbook 应用程序中看到的那样。
在本节中,你学习了如何创建用于用户状态和喜欢图片的 Redux 存储库。我们为存储库的两个部分添加了 reducer 以及动作。我们利用 Redux Toolkit 提供的一些实用工具使我们的工作更轻松。我们将所有这些整合在一起,最终能够减少一些 React 的上下文。用Redux替换所有其他上下文是一个很好的练习,可以帮助你熟悉这个状态管理库。如果你只想看看它看起来会是什么样子,请查看书籍仓库和文件夹:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-5-complete。
现在,我们将探讨在使用 Redux 时可能出现的处理问题和调试问题。
调试
我们的 Funbook 应用程序目前相当简单。然而,当与更大的应用程序一起工作时,你会注意到随着每个新功能的添加,状态变得越来越复杂。有时,功能具有重叠的状态或复杂的操作,负责应用程序中发生的许多事情。为了追踪与复杂状态变化相关的错误,我们可以使用专门的调试器。在裸Redux应用程序中配置开发工具需要几个步骤,但我们使用的是Redux Toolkit!它再次伸出援手。Redux Toolkit预先配置为与Redux DevTools 扩展一起工作,该扩展在浏览器中运行。由于我们正在开发 React Native 应用程序,我们需要使用另一个工具,称为React Native调试器。Mac 用户可以使用 Homebrew 工具安装它:
brew install react-native-debugger
如果你不是使用 Mac 电脑,你将在他们的安装说明页面上找到一个预构建的二进制文件:github.com/jhen0409/react-native-debugger。
一旦远程调试器安装完成,你可以在终端中输入以下命令来运行它:
open "rndebugger://set-debugger-loc?host=localhost&port=8081"
由于我们使用的是Expo,我们需要做一些更改才能实际调试我们的应用程序。到目前为止,默认配置的React Native调试工具还没有找到我们的应用程序:

图 5.3 – 安装后的 React Native 调试器
我们需要告诉19000。你可能需要停止调试器和应用程序,然后运行以下命令以在正确的端口上打开 React Native 调试器:
open "rndebugger://set-debugger-loc?host=localhost&port=19000"
最后,通过在终端中停止服务器并重新运行以下命令来重新启动应用程序:
expo start
React Native调试器是一个非常有用的工具,不仅用于调试 Redux,还用于检查React Native应用程序中的各种错误。
在本节中,我们讲解了如何安装和使用React Native调试工具。我鼓励你探索这个非常有用的工具,检查应用,也许可以添加一些错误的代码来看看这个工具中的错误可能是什么样子。
摘要
在我们穿越状态管理生态系统的旅途中我们已经走了很长的路。在本章中,我们以example-app-full作为起点,讨论了被认为是该应用中最常见的状态管理解决方案,并尝试用Redux替换LikedImages上下文。
进一步阅读
-
redux.js.org/introduction/why-rtk-is-redux-today– 为什么使用 Redux Toolkit? -
redux.js.org/introduction/core-concepts– Redux 核心概念。 -
blog.isquaredsoftware.com/2016/09/how-i-got-here-my-journey-into-the-world-of-redux-and-open-source/– Mark Erikson 关于他成为 Redux 维护者的博客。 -
blog.isquaredsoftware.com/2018/03/redux-not-dead-yet/– Redux 并未死去。 -
egghead.io/courses/fundamentals-of-redux-course-from-dan-abramov-bd5cc867– Dan Abramov 的 Egghead 教程。 -
stackoverflow.com/a/34582848/8798164– 关于状态变更的 Stack Overflow 回答。 -
redux.js.org/tutorials/essentials/part-2-app-structure#rules-of-reducers– Reducers 的规则。 -
daveceddia.com/what-is-a-thunk/– 什么是 thunk?
第六章:在 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!这正是本书的真正目的:了解不同的解决方案,以便您可以为未来的项目选择您更喜欢的方案。
技术要求
为了跟随本章的内容,您需要了解一些 JavaScript 和 ReactJS 的知识。如果您至少跟完了本书的 第一章 到 第四章,您应该能够无任何问题地继续前进。
随意使用您选择的 IDE,因为 React Native 不需要任何特定功能。目前,前端开发者中最受欢迎的 IDE 是微软的 VSCode、Atom、Sublime Text 和 WebStorm。
本章提供的代码片段旨在说明我们应该如何使用代码。它们并不提供完整的画面。为了更容易地编码,请打开您 IDE 中的 GitHub 仓库并查看其中的文件。您可以从名为 example-app-full 或 chapter-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-Tree(MST),这是 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 说,他希望改进库的TypeScript(TS)类型,但总体来说,他认为由于作者 Michel Weststrate 出色的工程,这个库表现得非常好。
MobX 目前是 React 应用程序中最受欢迎的状态管理库之一。文档中提到,它是最受欢迎的 Redux 替代方案之一。如果你仔细阅读文档,可能会发现作者暗示 MobX 比起 Redux 更好。当我问及这种竞争关系时,Jamon 说:“与其他优秀的社区争论总是很有趣。现实是,MobX 社区非常尊重 Redux 社区。他们的社区推动我们变得更好,并不断进步。他们做出了不同的权衡决策,可能不是你的特定风格,所以有选择权是件好事。”
当然,MobX 维护者有完全的权利认为他们正在工作的解决方案更好。现在,让我们看看你,亲爱的读者,是怎么想的!
关于 MobX 的概念和高级理念,文档中有一句非常重要的话被加粗了:
应该从应用状态中推导出任何可以推导的东西。自动地。
- MobX 口号
这是一个新概念!任何可以推导出的东西都应该自动推导。我们之前是否自动从我们的应用状态中推导出任何东西?实际上并没有。最初,我们创建了 useState 和 useEffect 钩子,与 React 上下文结合使用。每当用户与我们的应用交互时,我们必须手动更新所有必要的状态部分。在 Redux 中,我们编写了动作,并将状态更新的信息传递给 reducer。我们可以说状态更新是自动发生的;在传递动作后,我们不需要执行任何额外的任务。然而,我们确实创建了动作并手动调用它。我们还知道 Redux 并不特别提倡从应用状态中推导值。Redux 文档更多地集中在不可变性、状态是单一真相来源以及使用纯函数。
MobX 文档指出,这个库基于透明的函数式编程——这一概念在由 Packt Publishing 出版的《MobX 快速入门指南》一书中得到了进一步解释。MobX 的哲学是以下这些:
-
简单直接 – 编写简约的代码,反应系统将自动检测所有更改,无需添加特殊工具或样板代码。
-
轻松优化 – 数据更改在运行时跟踪,这意味着计算只在需要时运行,我们避免了不必要的组件重新渲染。
-
无偏见 – 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
此命令将安装 MobX 和 MobX-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-tree 的 types。这些类型是 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 文件,并告诉它使用 User 和 LikedImages 模型。我们将首先导入所有必要的文件并创建一个空的存储:
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: {}
})
如您所记,亲爱的读者,MobX 和 MST 在 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 – 没有图片的收藏表面
在 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数组中),以及何时进行操作(当存储创建时)。如果你现在检查应用,你应该能看到正确渲染的图像列表。
我们编写的代码运行良好,但我们可以进一步改进它。MobX和MST为我们提供了编写异步逻辑的优化解决方案。他们的解决方案被称为生成器函数。一开始这可能听起来有些吓人,但别担心。我们只需要从 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!
进一步阅读
-
mobx.js.org/README.html:MobX 文档。 -
mobx-state-tree.js.org/intro/welcome:MobX-State-Tree。 -
egghead.io/courses/manage-application-state-with-mobx-state-tree: 使用 Mobx-state-tree 管理应用程序状态。 -
www.packtpub.com/product/mobx-quick-start-guide/9781789344837: MobX 快速入门指南。 -
github.com/infinitered/ignite: Infinite Red 的 React Native 模板 - Ignite。 -
reactnativeradio.com/episodes/rnr-241-redux-toolkit-vs-mobx-state-tree-showdown: Redux Toolkit 与 MobX-State-Tree 对比。 -
www.loom.com/share/9e3afe0547824e42bada06191e891ae1: 由 Jamon Holmgren 撰写的 MobX-State-Tree 和 MobX-React 入门。 -
mobx.js.org/collection-utilities.html: MobX 集合工具。
第七章:使用 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-full 或 chapter-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.2 – 复杂状态图
用户点击一个有效的输入并进入有效启用未更改状态。应用会自动过渡到无效启用未更改状态。当用户提供一些输入时,应用将处于无效启用更改状态。如果用户提供的输入有效,我们将进入有效启用更改状态;如果不有效,我们将返回到无效启用更改状态。如果用户在表单中点击其他内容——比如说,一个禁用第一个输入的单选框?我们将进入无效(或有效)禁用更改状态。对这个图表进行推理相当困难。这就是状态图特性发挥作用的时候。状态图提供了并行状态、层次结构和守卫的实现。您可以在 XState 文档中推荐的这篇文档中了解更多关于这些概念的信息:statecharts.dev/state-machine-state-explosion.html。
XState 背后的最后一个重要概念是演员模型。这是一个计算数学模型,表明一切都是一个“演员”,并且可以执行三件事情:接收消息、发送消息以及处理接收到的消息。
我非常幸运能够就 XState 库的主题向其作者大卫·库尔希德提出几个问题。他告诉我他“创建 XState 有两个原因:管理和可视化复杂逻辑。状态机和状态图是视觉形式化工具,擅长以直观的方式表示甚至是最复杂的流程和逻辑,并且我希望在 JavaScript 应用中使用它们的方式简单。”他还补充说,XState 的高级理念受到了万维网联盟(W3C)状态图 XML(SCXML)规范的强烈影响。
让我们快速了解一下 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 与其他状态管理库截然不同的东西。多亏了这个可视化器,你可以在应用中看到状态和状态之间转换的图形表示。你可以用它来规划新的应用或调试你正在工作的应用。你可以在 https://xstate.js.org/viz/ 找到这个可视化器。以下是一个示例屏幕截图,展示了它的样子:

图 7.3 – XState 可视化器的屏幕截图
大卫说,可视化器是他工作过的最难的事情之一。它始终处于进行中,已经经历了多次迭代。目前,它是一个“基于 SVG 的 '画布',内部包含 HTML。" 尽管现在它有一定的交互性——你可以点击转换并观察状态如何变化——大卫说,“使其交互式是另一个难度层,特别是对于拖放交互和修改状态图。" 我个人对可视化器的最新版本非常兴奋。它已经多次帮助我规划我为应用(使用 XState)设计的最佳状态机。
在本节中,我们讨论了 XState 背后的主要思想。它们与我们之前分析的所有方法都不同。整个库基于有限状态机的数学概念。它还使用了状态图和演员模型背后的理论,以确保在复杂应用中管理状态可以有效地进行。现在,是时候看看这个库的实际应用了。让我们继续在 Funbook 应用中实现 XState。
在 Funbook 应用中配置 XState
让我们看看在真实应用中使用 XState 需要什么。如果你想亲自跟随,你可以复制 example-app-full 文件夹并将其用作起点。如果你想查看与本章相关的代码,请查看 chapter-7 文件夹:https://github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-7。
首先——我们需要将 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 转换来退出这个状态。你可以继续改进这个例子,通过添加一些 LOGIN 和 LOGOUT 转换的实现细节,或者可能是一个错误状态。但我现在将停止讨论这个特定的状态机,看看它应该如何在 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 错误
获取图像数据
获取数据并不总是状态管理库的强项。毕竟,这并不是它们的基本职责。然而,在 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属性的值是一个包含要调用的函数的id和src属性的对象。可以调用 Promise、回调(可以发送和接收来自父机器的事件)——可以发送事件到父机器——以及整个机器。我们将保持简单,并在源中添加一个异步的fetch函数。你还可以在任何机器外部创建一个命名函数,并通过src调用它。我们还使用了invoke属性的两个可选值:onDone和onError。这两个转换在处理 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达到其最终状态时将被调用。这个函数将调用机器返回的数据分配给likeImagesMachine的context,并通过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,我们需要添加一个将更新此上下文值的动作。我们将在 likeImagesMachine 的 ready 状态中添加一个名为 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 的自定义钩子来消费我们之前设置的上下文值。然后,我们使用来自 likedImagesAppService 的 send 函数。最后,我添加了一个 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 函数,如果它是 null 或 undefined,这会导致应用程序崩溃。一旦我们确定图片数组存在,我们就可以通过当前图片过滤它。你可以把这个函数放在任何你想放的地方(与机器相同的文件中,在名为 selectors 或 utilities 的文件中,等等),然后将其导入到 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。
进一步阅读
-
https://www.w3.org/TR/scxml/: W3C SCXML 规范。
-
https://xstate.js.org/docs/recipes/react.html#local-state: XState 菜谱。
-
https://xstate.js.org/docs/guides/context.html#assign-action: 分配动作。
第八章:在 React Native 应用中集成 Jotai
在上一章中,我们探索了XState的数学世界。我们将继续我们的旅程,通过探索另一个名为Jotai的年轻状态管理库来继续前进。Jotai受到了在Facebook创建的一个实验性状态管理库 Recoil 的启发。在本章中,我们将简要了解Recoil,这是 Facebook 创建的一个实验性状态管理库。一旦我们熟悉了这个库的主要思想,即一个名为“原子状态”的新概念,我们将深入探讨Jotai。我们将在我们的应用中配置 Jotai,并借助Jotai继续进行数据获取和管理点赞图片的工作。以下是本章我们将涉及的内容:
-
Recoil和原子状态是什么?
-
什么是Jotai?
-
在 Funbook 应用中配置Jotai
-
使用
FavoritedImages
到本章结束时,你将有一种新的看待全局状态管理的方法——通过将其划分为称为原子的小项目。你还将了解如何在新的项目中设置Jotai,以及如何使用它进行数据获取和数据管理。
技术要求
为了跟上本章的内容,你需要了解一些JavaScript和ReactJS的知识。如果你至少跟随着本书的第一章到第四章,你应该能够没有问题地继续学习。
随意使用你选择的 IDE,因为React Native不需要任何特定功能。目前,前端开发者中最受欢迎的 IDE 是微软的 VSCode、Atom、Sublime Text 和 WebStorm。
本章提供的代码片段旨在说明我们应该如何使用代码。它们并不提供完整的画面。为了更容易地编码,请在你选择的 IDE 中打开 GitHub 仓库,查看其中的文件。你可以从名为example-app-full或chapter-8的文件夹中的文件开始。如果你从example-app-full开始,你将负责实现本章描述的解决方案。如果你选择查看chapter-8,你将看到我实现的整个解决方案。
如果你遇到困难或迷失方向,可以查看 GitHub 仓库中的代码:github.com/PacktPublishing/Simplifying-State-Management-in-React-Native/tree/main/chapter-8。
Recoil和原子状态是什么?
如果你一直按章节顺序阅读这本书,你可能觉得不同类型的状态管理库列表永远没有尽头。在某种程度上,这是正确的。每隔几周就会出现新的状态管理库;它们有时是纯开源的,有时是公司支持的。然而,它们很少提出突破性的解决方案。更常见的是,它们是已知概念的较新实现。这些实现受到了极大的欢迎,因为每个开发者都喜欢舒适地工作——那么,那些已知的概念是什么呢,你可能想知道?
在ReactJS世界中,有一个共识,即状态管理库可以分为三种类型:
-
Flux 类型 - 这些是持有状态在组件之外的状态管理库,并使用单向数据流。它们受到了Facebook 的 Flux的启发,最著名的例子是Redux。这种流有现代的实现,如Redux Toolkit或Zustand。
-
代理类型 - 这些库“包装”状态,概念上类似于代理所做的那样。当使用这种类型的状态管理时,开发者可以像读取组件中的任何其他值一样订阅和读取包装值。代理类型状态管理的最佳例子是React 的 Context、MobX或Valtio。
-
原子类型 - 这是最低级别的状态集合,由类组件中的
setState和函数组件中的useState钩子自然管理。以这种方式设置的价值可以在应用程序中传递并用于更大的上下文中。Facebook创建了一个实验性库来推广这种类型的状态管理,称为Recoil。Jotai很快也效仿。
回弹(Recoil)是在 2020 年中期创建的,并迅速获得了大量关注。它是由 Facebook 自己发布的,Facebook 也是 React 的创造者,因此每个人都期待着一个全新的解决方案。使用尽可能小的状态片段,并在React应用中分散和可访问的想法非常吸引人。不幸的是,在最初的兴奋之后,React 社区中很大一部分人对Recoil失去了兴趣,继续使用 Redux 进行日常工作。两年后,Recoil的文档仍然声明它是实验性的,而且很少有人谈论它。
尽管如此,有一小群开发者比我们其他人更加关注。Poimandres,一个开源开发者集体,开始工作并创建了他们自己的原子状态实现。他们称之为Jotai。如果你访问他们的 GitHub 页面,你会看到他们也开发了Valtio,一个代理类型的状态管理库,以及Zustand,一个轻量级的 flux 类型状态管理库。Valtio和Zustand到目前为止在更著名的替代品阴影之下,但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 原子的应用显示图像
一切都正常!我必须承认,这感觉几乎像魔法。当然,获取数据会更复杂吗?
使用 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 组件的生命周期有一些微妙之处。在类组件中,这些微妙之处更为明显,我们会使用componentDidMount、componentWillUnmount等。函数组件有相同的生命周期,但没有那么明显。而且,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时遇到过这个问题。FlatList的extraData属性——我们可以将原子值传递给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 社区中越来越受欢迎。它提供了一种自下而上的方法,与自上而下的库(如Redux或MobX)相反。它确实非常容易配置和使用。它不提供很多工具,但文档非常清晰且易于使用。在本章中,我们成功地使用它来获取和存储数据,我们还用它来实现对数据的操作,例如向数组中添加项目。Jotai标志着我们与经典状态管理库的旅程的结束。
在下一章中,我们将讨论React Query,它不是一个状态管理库,而是一个数据获取库。然而,它在这本书中也有其位置。更多内容将在下一章中介绍!那里见!
进一步阅读
-
marmelab.com/blog/2022/06/23/proxy-state-with-valtio.html: 状态管理之旅:使用 Valtio 进行代理状态。 -
github.com/facebookexperimental/Recoil/tree/main: Recoil GitHub 页面。 -
opencollective.com/pmndrs: Poimandres 网站。 -
github.com/dai-shi/react-suspense-fetch:react-suspense-fetch. -
github.com/dai-shi/react-hooks-fetch:react-hooks-fetch。 -
github.com/dai-shi/react-hooks-worker:react-hooks-worker。 -
jotai.org/docs/guides/async: Jotai – 异步。 -
discord.com/invite/poimandres: Poimandres Discord 服务器。 -
www.geekyhub.in/post/difference-between-useeffect-and-direct-function-call/: “useEffect”与在组件内部直接调用函数之间的区别。 -
reactnavigation.org/docs/function-after-focusing-screen/#re-rendering-screen-with-the-useisfocused-hook: React NavigationuseIsFocused钩子。
第九章:使用 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 处理数据获取,并对该库的其他功能有一般了解。
技术要求
为了跟上本章的内容,你需要具备一些JavaScript和ReactJS的知识。如果你已经阅读了本书的至少第一章到第四章,你应该能够无任何问题地继续前进。
随意使用你选择的 IDE,因为React Native不需要任何特定功能。目前,前端开发者中最受欢迎的 IDE 包括微软的 VSCode、Atom、Sublime Text 和 WebStorm。
本章提供的代码片段旨在说明我们应该如何使用代码。它们并不提供完整的画面。为了在阅读本章的同时获得更好的编码体验,请在你的 IDE 中打开 GitHub 仓库,查看其中的文件。你可以从名为example-app-full或chapter-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 导入必要的函数——QueryClient 和 QueryClientProvider。然后,我们将创建一个新的 QueryClient 函数并将其传递给 QueryClientProvider。我们的应用现在可以使用 React Query 的功能,而不是简单的获取。
这是一个确保你的应用在模拟器或设备上正确运行的好时机。
一旦你确认安装新的依赖项没有在你的项目中造成意外的破坏,我们就可以在下一节中实现使用 React Query 的真实数据获取了。
使用 React Query 进行数据获取
正如你所知,我们需要为我们的应用获取一些不同的数据。我们将获取头像列表、用于动态界面的图片列表、用于“收藏的图片”界面的图片列表以及会话列表。我们可以自由地在任何地方添加 React Query 的获取操作。对于简单的查询,我们可以在组件中使用库提供的 useQuery 钩子。我们也可以编写自己的自定义钩子,包含更多的逻辑或条件。让我们从一个最简单的例子开始:查询服务器以检查用户是否已登录。
为了在设置导航以显示登录屏幕或否的顶层组件中使用 React Query 钩子,我们需要稍微重新组织一下我们的代码。我们不能在同一个组件的返回语句中同时使用 QueryClientProvider 和 useQuery 钩子。让我们将主组件的名称从 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 } = objectWithADataItem 与 const 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 实用工具中。这个实用工具为我们提供了一些东西,比如它有以下状态:isIdle、isLoading、isError和isSuccess。我们可以检查这些状态并根据情况更新视图。我们将在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组件,显示isSucccess和isError,你可能会更优雅地处理加载。
这听起来很棒,但按照我们上面实现突变的方式,我们仍然需要传统地重新获取数据,以便在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 应用状态管理主题上所学的所有内容。
进一步阅读
-
tanstack.com/– TanStack 主页。 -
tanstack.com/query/v4/docs/guides/updates-from-mutation-responses– TanStack Query,来自 突变响应 的更新。
第四部分 – 摘要
在本部分中,读者将概述本书涵盖的所有不同解决方案。
本部分包括以下章节:
- 第十章,附录
附录
好吧,我亲爱的读者,我们已经到达了这本书的最后一部分:总结。我真诚地希望你喜欢阅读我关于 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 前辈出现之前,创建在Android和iOS上工作的移动应用要复杂得多!
第五章,在我们的 Funbook 应用中实现 Redux,是第一个讨论 React Native 应用中状态管理外部解决方案的章节。我们讨论的具体解决方案是Redux和Redux 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。我希望能在这几个月和几年内有所改变,因为其他状态管理库提供了很好的解决方案。以下是一些常见或有趣的问题:
-
在 React 中,
props和state之间的区别是什么? -
在 React Native 应用程序中,使用外部状态管理库是必要的吗?
-
在 Redux 中,什么是 reducer 和 action?
-
在 Redux 中,使用 selectors 的优势是什么?
-
在 Redux 中,你能否直接更改状态值?
-
在 MobX 中,什么是模型?
-
在 MobX 中,你如何使组件感知全局状态值?
-
在 Xstate 中,什么是状态机?
-
在 Xstate 中,你如何通过状态机传递额外的数据?
-
在 Jotai 中,最基本的州状态叫什么名字?
-
你能否只用 React Query 就替换所有的状态管理?
我只给你这些问题,因为给你答案可能会太简单了,你不这么认为吗?如果你必须回到书中去研究答案,或者也许简单地谷歌一下,那么信息更有可能留在你脑海中。
我衷心希望您阅读这本书的乐趣和我写作这本书的乐趣一样!感谢您一直陪伴在这里,并且随时可以通过 Twitter(如果这本书出版时它仍然存在)联系我!晚安,祝您好运!


浙公网安备 33010602011771号