在本章中,我们将逐步介绍向应用添加 Redux 的过程。我们将介绍以下配方:
- 安装 Redux 并准备我们的项目
- 定义动作
- 定义减速器
- 开设店铺
- 与远程 API 通信
- 将存储连接到视图
- 使用 Redux 存储脱机内容
- 显示网络连接状态
在开发大多数应用的过程中,我们需要一种更好的方法来处理整个应用的状态。这将简化跨组件的数据共享,并为将来扩展我们的应用提供更强健的体系结构。
为了更好地理解 Redux,本章的结构将不同于前几章,因为我们将通过所有这些配方创建一个应用。本章中的每个配方将取决于最后一个配方。
我们将构建一个用于显示用户帖子的简单应用,并将使用ListView
组件显示从 API 返回的数据。我们将使用之前在使用过的优秀模拟数据 APIhttps://jsonplaceholder.typicode.com
在此配方中,我们将在一个空应用中安装 Redux,并定义应用的基本文件夹结构。
我们需要一个新的空应用来制作这个食谱。我们叫它redux-app
。
我们还需要两个依赖项:redux
用于处理状态管理,react-redux
用于将 Redux 和 React Native 粘合在一起。您可以使用以下命令行安装它们:
yarn add redux react-redux
或者您可以使用npm
:
npm install --save redux react-redux
- 作为这个配方的一部分,我们将构建应用将使用的文件夹结构。让我们添加一个
components
文件夹,其中包含一个Album
文件夹,用于保存相册组件。我们还需要一个redux
文件夹来保存我们所有的 Redux 代码。 - 在
redux
文件夹中,我们添加一个index.js
文件用于 Redux 初始化。我们还需要一个photos
目录,包含一个actions.js
文件和一个reducer.js
文件。 - 目前,
App.js
文件将只包含一个Album
组件,我们将在后面定义:
import React, { Component } from 'react';
import { StyleSheet, SafeAreaView } from 'react-native';
import Album from './components/Album';
const App = () => (
<SafeAreaView style={styles.container}>
<Album />
</SafeAreaView>
);
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
export default App;
在入门中,我们安装了redux
和react-redux
库。react-redux
库包含将 Redux 与 React 集成所需的绑定。Redux 并非专门设计用于 React。您可以将 Redux 与任何其他 JavaScript 库一起使用。通过使用react-redux
,我们将能够无缝地将 Redux 集成到我们的 React 本机应用中。
在步骤 2中,我们创建了将用于应用的主文件夹:
components
文件夹将包含我们的应用组件。在本例中,我们只添加了一个Album
成分,以保持配方简单。redux
文件夹将包含所有与 Redux 相关的代码(初始化、操作和还原程序)。
在中大型应用中,您可能希望进一步分离 React 本机组件。React 社区标准将应用的组件分为三种不同的类型:
Components
:社区称之为表象成分。简单来说,这些组件不知道任何业务逻辑或 Redux 操作。这些组件只通过道具接收数据,应该可以在任何其他项目上重用。一个按钮或面板将是一个完美的代表性组件的例子。Containers
:这些组件直接从 Redux 接收数据,并能够调用操作。在这里,我们将定义组件,例如显示登录用户的标题。通常,这些组件在内部使用表示组件。Pages/Views
:这些是应用中使用容器和表示组件的主要模块。
有关构建 Redux 组件的更多信息,我推荐优秀的文章构建 React Redux 项目以实现可伸缩性和可维护性,链接如下*:*
我们还需要创建一个redux/photos
文件夹。在此文件夹中,我们将创建以下内容:
actions.js
文件,其中包含应用可以执行的所有操作。我们将在下一个食谱中更多地讨论行动。reducer.js
文件,其中包含管理 Redux 存储中数据的所有代码。我们将在以后的食谱中更深入地探讨这个问题。
操作是向存储发送数据的信息有效负载。使用这些操作是组件向 Redux store 请求或发送数据的唯一方式,Redux store 是整个应用的全局状态对象。动作只是一个普通的 JavaScript 对象。我们将定义返回这些操作的函数。返回动作的函数称为动作创建者。
在此配方中,我们将创建用于加载库的初始图像的操作。在此过程中,我们将添加硬编码数据,但稍后,我们将从 API 请求这些数据,以创建更现实的场景。
让我们继续处理上一个配方中的代码。确保按照这些步骤安装 Redux 并构建我们将用于此项目的文件夹结构。
- 我们需要为每个操作定义类型。打开
redux/photos/actions.js
文件。动作类型定义为以后可以在动作和还原器中引用的常量,如下所示:
export const FETCH_PHOTOS = 'FETCH_PHOTOS';
- 现在,让我们创建第一个动作创建者。每个动作都需要一个
type
属性来定义它,动作通常会有一个payload
属性的数据随动作一起传递。在此配方中,我们将硬编码一个模拟 API 响应,该响应由两个 photo 对象的数组组成,如下所示:
export const fetchPhotos = () => {
return {
type: FETCH_PHOTOS,
payload: {
"photos": [
{
"albumId": 2,
"title": "dolore esse a in eos sed",
"url": "http://placehold.it/600/f783bd",
"thumbnailUrl": "http://placehold.it/150/d83ea2",
"id": 2
},
{
"albumId": 2,
"title": "dolore esse a in eos sed",
"url": "http://placehold.it/600/8e6eef",
"thumbnailUrl": "http://placehold.it/150/bf6d2a",
"id": 3
}
]
}
}
}
- 我们需要一个动作创建者为每个动作,我们希望应用能够执行,我们希望这个应用能够添加和删除图像。首先,我们添加
addBookmark
动作创建者,如下所示:
export const ADD_PHOTO = 'ADD_PHOTO';
export const addPhoto = (photo) => {
return {
type: ADD_PHOTO,
payload: photo
};
}
- 同样,我们需要另一个 action creator 来删除照片:
export const REMOVE_PHOTO = 'REMOVE_PHOTO';
export const removePhoto = (photo) => {
return {
type: REMOVE_PHOTO,
payload: photo
};
}
在步骤 1中,我们定义了动作的类型以指示它的功能,在本例中是获取图像。我们使用常量,因为它将在多个位置使用,包括动作创建者、还原者和测试。
在步骤 2中,我们声明了一个动作创建者。动作是简单的 JavaScript 对象,它定义了在我们的应用中发生的会影响应用状态的事件。我们使用动作与 Redux 存储中的数据交互。
只有一个要求:每个动作必须有一个type
属性。此外,一个操作通常会包含一个payload
属性,该属性保存与该操作相关的数据。在本例中,我们使用的是照片对象数组。
An action is valid as long as the type
property is defined. If we want to send anything else, it is a common convention to use the payload
property as popularized by the flux pattern. However, the name property isn't inherently special. We could name this params
or data
and the behavior would remain the same.
目前,我们已经定义了动作创建者,它们是返回动作的简单函数。为了使用它们,我们需要使用 Reduxstore
提供的dispatch
方法。我们将在以后的食谱中了解更多有关该店的信息。
现在,我们已经为我们的应用创建了一些操作。正如前面所讨论的,动作定义了应该发生的事情,但是我们没有创建任何东西来将动作付诸实施。这就是减速机的用武之地。Reducer 是定义操作如何影响 Reduxstore
中数据的函数。store
中的所有数据访问都发生在一个减速器中。
减速器接收两个参数:state
和action
。state
参数表示 app 的全局状态,action
参数是减速器正在使用的动作对象。减速器返回一个新的state
参数,反映与给定action
参数相关的变化。在这个配方中,我们将引入一个 reducer,通过使用前面配方中定义的操作来获取照片。
此配方取决于上一配方*定义动作**。*请务必从本章开头开始,以避免出现任何问题或混淆。
- 首先,我们打开
photos/reducer.js
文件,导入前面配方中定义的所有动作类型,如下所示:
import {
FETCH_PHOTOS,
ADD_PHOTO,
REMOVE_PHOTO
} from './actions';
- 我们将在此减速器中为状态定义一个初始状态对象。对于当前加载的照片,它有一个初始化为空数组的
photos
属性,如下所示:
const initialState = () => return {
photos: []
};
- 我们现在可以定义
reducer
函数。它将接收两个参数,当前状态和已调度的操作,如下所示:
export default (state = initialState, action) => {
// Defined in next steps
}
React Native components can also have a state
object, but that is an entirely separate state
from that which Redux uses. In this context, state
refers to the global state stored in the Redux store
.
- 状态是不可变的,因此在 reducer 函数中,我们需要为当前操作返回一个新状态,而不是操纵状态,如下所示:
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_PHOTOS:
return {
...state,
photos: [...action.payload],
};
// Defined in next steps
}
- 为了向数组中添加一个新书签,我们需要做的就是获取操作的有效负载并将其包含在新数组中。我们可以使用 spread 操作符在
state
上展开当前照片数组,然后将action.payload
添加到新数组中,如下所示:
case ADD_PHOTO:
return {
...state,
photos: [...state.photos, action.payload],
};
- 如果要从数组中删除项,可以使用 filter 方法,如下所示:
case REMOVE_PHOTO:
return {
...state,
photos: state.photos.filter(photo => {
return photo.id !== action.payload.id
})
};
- 最后一步是合并我们所有的减速器。在一个更大的应用中,你可能有理由将你的还原程序分解成不同的文件。由于我们只使用一个减速器,这一步在技术上是可选的,但它说明了如何将多个减速器与 Redux 的
combineReducers
助手组合在一起。让我们在redux/index.js
文件中使用它,我们也将在下一个配方中使用它来启动 Redux 存储,如下所示:
import { combineReducers } from 'redux';
import photos from './photos/reducers';
const reducers = combineReducers({
photos,
});
在步骤 1中,我们导入了前面配方中声明的所有动作类型。我们使用这些类型来确定应该采取什么行动以及action.payload
应该如何影响 Redux 状态。
在步骤 2中,我们定义了reducer
函数的初始状态。目前,我们的照片只需要一个空数组,但我们可以向状态中添加其他属性,例如isLoading
和didError
的布尔属性来跟踪加载和错误状态。反过来,这些可用于在async
操作期间和响应async
操作时更新 UI。
在步骤 3中,我们定义了reducer
函数,该函数接收两个参数:当前状态和正在调度的动作。如果没有提供初始状态,我们将初始状态设置为initialState
。通过这种方式,我们可以确保应用中始终存在照片阵列,这将有助于避免在调度操作时出现错误,而不会影响 Redux 状态。
在步骤 4中,我们定义了一个获取照片的操作。记住,状态永远不会被直接操纵。如果动作的类型与案例匹配,则通过将当前的state.photos
数组与action.payload
上的传入照片相结合来创建一个新的状态对象。
reducer
函数应该是纯函数。这意味着对任何输入值都不应有副作用。改变状态或行为是不好的做法,应该始终避免。突变可能导致数据不一致或无法正确触发渲染。此外,为了防止副作用,我们应该避免在 reducer 中执行任何 AJAX 请求。
在步骤 5中,我们创建了向照片数组添加新元素的操作,但我们没有使用Array.push
,而是返回一个新数组,并将传入元素附加到最后一个位置,以避免在状态上改变原始数组。
在步骤 6中,我们添加了一个从状态中删除书签的操作。最简单的方法是使用filter
方法,这样我们就可以忽略动作有效负载上接收到的 ID 为的元素。
在步骤 7中,我们使用combineReducers
函数将所有的约简合并为一个全局状态对象,该对象将保存在存储中。此函数将调用键处于与该减速机对应状态的每个减速机;此功能与以下功能完全相同:
import photosReducer from './photos/reducer';
const reducers = function(state, action) {
return {
photos: photosReducer(state.photos, action),
};
}
只有关心照片的州才会调用照片缩减器。这将帮助您避免在一个简化程序中管理所有状态数据
Redux 存储负责更新根据减速器内部状态计算的信息。它是一个单一的全局对象,可以通过商店的getState
方法访问。
在这个配方中,我们将把我们在前面的配方中创建的动作和减速器联系在一起。我们将使用现有操作来影响存储中的数据。我们还将学习如何通过订阅存储更改来记录状态的更改。这个配方更像是行动、减速器和商店如何协同工作的概念证明。在本章后面,我们将深入探讨 Redux 如何在应用中更常用。
- 我们打开
redux/index.js
文件,从redux
导入createStore
函数,如下所示:
import { combineReducers, createStore } from 'redux';
- 创建商店非常简单;我们只需调用步骤 1中导入的函数 ,并将减速机作为第一个参数发送,如下所示:
const store = createStore(reducers);
export default store;
- 就这样!我们已经建立了商店,现在让我们发布一些行动。此配方中的后续步骤将从最终项目中删除,因为它们是用于测试我们的设置。我们先导入要分派的动作创建者:
import {
loadPhotos,
addPhotos,
removePhotos,
} from './photos/actions';
- 在分派任何操作之前,让我们订阅存储,这将允许我们监听存储中发生的任何更改。就我们目前的目的而言,我们只需要
console.log
对store.getState()
的结果进行console.log
,如下所示:
const unsubscribe = store.subscribe(() => {
console.log(store.getState());
});
- 让我们分派一些操作,并在开发人员控制台中查看结果状态:
store.dispatch(loadPhotos());
- 为了添加一个新书签,我们需要以 photos 对象作为参数来调度
addBookmark
action creator:
store.dispatch(addPhoto({
"albumId": 2,
"title": "dolore esse a in eos sed",
"url": `http://placehold.it/600/`,
"thumbnailUrl": `http://placehold.it/150/`
}));
- 要删除项目,我们将要删除的照片的
id
传递给 action creator,因为这是 reducer 用来查找应该删除的项目的方法:
store.dispatch(removePhoto({ id: 1 }));
- 在执行所有这些操作后,我们可以通过在订阅存储时运行步骤 4中创建的取消订阅功能来停止监听存储上的更改,如下所示:
unsubscribe();
- 我们需要将
redux/index.js
文件导入App.js
文件,该文件将运行此配方中的所有代码,以便我们可以在开发者控制台中看到相关的console.log
消息:
import store from './redux';
在步骤 3中,我们导入了我们在前面的配方定义动作中创建的动作创建者。即使我们还没有 UI,我们也可以使用 Redux 存储并观察发生的更改。它只需要调用一个动作创建者,然后分派生成的动作。
在步骤 5中,我们从store
实例调用了dispatch
方法。dispatch
执行一个动作,该动作由loadBookmarks
动作创建者创建。减速器将依次调用,这将设置状态的新照片。
一旦我们有了 UI,我们将以类似的方式从组件中分派操作,这将更新状态,最终触发组件的重新呈现,显示新数据。
我们目前正在从动作中的硬编码数据加载书签。在真正的应用中,我们更有可能从 API 获取数据。在此配方中,我们将使用 Redux 中间件帮助从 API 获取数据。
在这个配方中,我们将使用axios
发出所有 AJAX 请求。用npm
安装:
npm install --save axios
也可以使用yarn
进行安装:
yarn add axios
对于这个配方,我们将使用 Redux 中间件redux-promise-middleware
。使用npm
安装软件包:
npm install --save redux-promise-middleware
也可以使用yarn
进行安装:
yarn add redux-promise-middleware
该中间件将为应用中的每个 AJAX 请求创建并自动调度三个相关操作:一个是在请求开始时,一个是在请求成功时,另一个是在请求失败时。使用这个中间件,我们能够定义一个 action creator,该 action creator 返回一个 action 对象,并为有效负载提供一个承诺。在本例中,我们将创建async
操作FETCH_PHOTOS
,其有效负载是一个 API 请求。中间件将创建并分派一个FETCH_PHOTOS_PENDING
类型的操作。当请求解析时,如果请求成功,中间件将创建并调度解析数据为payload
的FETCH_PHOTOS_FULFILLED
类型的操作;如果请求失败,中间件将创建并调度错误为payload
的FETCH_PHOTOS_REJECTED
类型的操作。
- 让我们首先将新的中间件添加到我们的 Redux 存储中。在
redux/index.js
文件中,我们添加 Redux 方法applyMiddleware
。我们还将添加刚刚安装的新中间件,如下所示:
import { combineReducers, createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise-middleware';
- 在前面定义的对
createStore
的调用中,我们可以传入applyMiddleware
作为第二个参数。applyMiddleware
取一个参数,就是我们要使用的中间件promiseMiddleware
:
const store = createStore(reducers, applyMiddleware(promiseMiddleware()));
Unlike some other popular Redux middleware solutions such as redux-thunk
, promiseMiddleware
must be invoked when it is passed to applyMiddleware
. It is a function that returns the middleware.
- 我们现在将在我们的操作中发出真正的 API 请求,所以我们需要将
axios
导入redux/photos/actions
。我们还将添加 API 的基本 URL。我们使用的虚拟数据 API 与前几章中使用的相同,托管在http://jsonplaceholder.typicode.com ,具体如下:
import axios from 'axios';
const API_URL='http://jsonplaceholder.typicode.com';
- 接下来,我们将更新我们的动作创建者。我们将首先更新处理 AJAX 请求所需的类型,如下所示:
export const FETCH_PHOTOS = 'FETCH_PHOTOS';
export const FETCH_PHOTOS_PENDING = 'FETCH_PHOTOS_PENDING';
export const FETCH_PHOTOS_FULFILLED = 'FETCH_PHOTOS_FULFILLED';
export const FETCH_PHOTOS_REJECTED = 'FETCH_PHOTOS_REJECTED';
- 我们将返回一个
GET
请求,而不是将此操作的虚拟数据返回为payload
。由于这是一个Promise
,它将触发我们的新中间件。另外,请注意动作的类型是如何的FETCH_PHOTOS
。这将导致中间件在成功时自动创建FETCH_PHOTOS_PENDING
、FETCH_PHOTOS_FULFILLED
和payload
已解析数据,以及FETCH_PHOTOS_REJECTED
和payload
已发生错误,如下所示:
export const fetchPhotos = () => {
return {
type: FETCH_PHOTOS,
payload: axios.get(`${API_URL}/photos?_page=1&_limit=20`)
}
}
- 与
FETCH_PHOTOS
操作一样,我们将使用为ADD_PHOTO
操作提供的相同中间件类型,如下所示:
export const ADD_PHOTO = 'ADD_PHOTO';
export const ADD_PHOTO_PENDING = 'ADD_PHOTO_PENDING';
export const ADD_PHOTO_FULFILLED = 'ADD_PHOTO_FULFILLED';
export const ADD_PHOTO_REJECTED = 'ADD_PHOTO_REJECTED';
- action creator 本身将不再将传入的照片作为
payload
返回,而是通过 API 传递添加图像的POST
请求承诺,如下所示:
export const addPhoto = (photo) => {
return {
type: ADD_PHOTO,
payload: axios.post(`${API_URL}/photos`, photo)
};
}
- 我们可以按照相同的模式将
REMOVE_PHOTO
操作转换为 AJAX 请求,该请求使用 API删除照片。与ADD_PHOTO
和FETCH_PHOTOS
的其他两个动作创建者一样,我们将为每个动作定义动作类型,然后将删除axios
请求作为动作的payload
返回。由于从 Redux 存储中删除图像对象时,我们需要在 reducer 中使用photoId
,因此我们还将其作为操作的meta
属性上的对象传递,如下所示:
export const REMOVE_PHOTO = 'REMOVE_PHOTO';
export const REMOVE_PHOTO_PENDING = 'REMOVE_PHOTO_PENDING';
export const REMOVE_PHOTO_FULFILLED = 'REMOVE_PHOTO_FULFILLED';
export const REMOVE_PHOTO_REJECTED = 'REMOVE_PHOTO_REJECTED';
export const removePhoto = (photoId) => {
console.log(`${API_URL}/photos/${photoId}`);
return {
type: REMOVE_PHOTO,
payload: axios.delete(`${API_URL}/photos/${photoId}`),
meta: { photoId }
};
}
- 我们还需要重新检查减速器,以调整预期有效载荷。在
redux/reducers.js
中,我们将首先导入将要使用的所有动作类型,并更新initialState
。出于下一个配方中显而易见的原因,让我们将state
对象上的照片数组重命名为loadedPhotos
,如下所示:
import {
FETCH_PHOTOS_FULFILLED,
ADD_PHOTO_FULFILLED,
REMOVE_PHOTO_FULFILLED,
} from './actions';
const initialState = {
loadedPhotos: []
};
- 在减速器本身中,更新每个案例,以采用基础动作的
FULFILLED
变化:FETCH_PHOTOS
变为FETCH_PHOTOS_FULFILLED
、ADD_PHOTOS
变为ADD_PHOTOS_FULFILLED
、REMOVE_PHOTOS
变为REMOVE_PHOTOS_FULFILLED
。我们还将state
的照片数组的所有引用从photos
更新为loadedPhotos
。当使用axios
时,所有响应对象将包含一个data
参数,该参数保存从 API 接收到的实际数据,这意味着我们还需要将action.payload
的所有引用更新为action.payload.data
。而在REMOVE_PHOTO_FULFILLED
减速器中,我们在action.payload.id
处再也找不到photoId
,这就是为什么我们在步骤 8中传递了动作的meta
属性photoId
,因此action.payload.id
变为action.meta.photoId
,如下所示:
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_PHOTOS_FULFILLED:
return {
...state,
loadedPhotos: [...action.payload.data],
};
case ADD_PHOTO_FULFILLED:
return {
...state,
loadedPhotos: [action.payload.data, ...state.loadedPhotos],
};
case REMOVE_PHOTO_FULFILLED:
return {
...state,
loadedPhotos: state.loadedPhotos.filter(photo => {
return photo.id !== action.meta.photoId
})
};
default:
return state;
}
}
在步骤 2中,我们应用了安装在入门部分的中间件。如前所述,该中间件将允许我们为 AJAX 操作创建一个操作创建者,自动为PENDING
、FULFILLED
和REJECTED
请求状态创建单个操作创建者
在步骤 5中,我们定义了fetchPhotos
动作创建者。您将从前面的方法中回忆起,操作是普通的 JavaScript 对象。由于我们在操作的有效负载属性上定义了一个承诺,redux-promise-middleware
将拦截此操作并自动为三个可能的请求状态创建三个关联操作。
在步骤 7和步骤 8中,我们定义了addPhoto
动作创建者和removePhoto
动作创建者,它们与fetchPhotos
一样,具有 AJAX 请求作为动作负载。
通过使用这个中间件,我们能够避免为了发出不同的 AJAX 请求而反复重复相同的样板文件。
在这个配方中,我们只处理在应用中发出的 AJAX 请求的成功条件。在真正的应用中,处理以_REJECTED
结尾的操作类型表示的错误状态是明智的。这将是一个处理错误的好地方,可以将错误保存到 Redux 存储中,这样视图就可以在错误发生时显示错误信息。
到目前为止,我们已经设置了状态,包括了中间件,并且定义了操作、操作创建者和还原器,用于与远程 API 交互。但是,我们无法在屏幕上显示任何这些数据。在此配方中,我们将使组件能够访问我们创建的存储。
此配方取决于之前的所有配方,因此请确保遵循此配方之前的每个配方。
在本章的第一个配方中,我们安装了react-redux
库以及其他依赖项。在这个食谱中,我们最终将利用它。
我们还将使用第三方库生成随机的彩色十六进制数,我们将使用该库从位于的占位符图像服务请求彩色图像 https://placehold.it/ 。在开始之前,请使用npm
安装randomcolor
:
npm install --save randomcolor
也可以使用yarn
进行安装:
yarn add randomcolor
- 让我们首先将 Redux 商店连接到
App.js
中的 React 本机应用。我们将从导入开始,从react-redux
导入Provider
和我们之前创建的存储。我们还将导入稍后定义的Album
组件,如下所示:
import React, { Component } from 'react';
import { StyleSheet, SafeAreaView } from 'react-native';
import { Provider } from 'react-redux';
import store from './redux';
import Album from './components/Album';
Provider
的工作是将我们的 Redux 商店连接到 React 本机应用,以便应用的组件可以与商店通信。Provider
应该用于包装整个应用,由于此应用位于Album
组件中,我们将用Provider
组件包装Album
组件。Provider
需要一个store
道具,我们将经过我们的 Redux 商店。应用和商店是有线连接的:
const App = () => (
<Provider store={store}>
<Album />
</Provider>
);
export default App;
- 让我们转到
Album
组件。该部件将在components/Album/index.js
处通电。我们先从进口开始。我们将导入用于生成随机颜色六角体的randomcolor
包,如入门部分所述。我们还将从react-redux
导入connect
,我们在前面的配方中定义的动作创建者connect
将我们的应用连接到 Redux 商店,然后我们可以使用动作创建者影响商店的状态,如下所示:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
SafeAreaView,
ScrollView,
Image,
TouchableOpacity
} from 'react-native';
import randomColor from 'randomcolor';
import { connect } from 'react-redux';
import {
fetchPhotos,
addPhoto,
removePhoto
} from '../../redux/photos/actions';
- 让我们创建
Album
类,但是,我们将使用connect
将Album
连接到商店,而不是直接将Album
导出为default
导出。注意,connect
用两组括号调用,组件被传递到第二组中,如下所示:
class Album extends Component {
}
export default connect()(Album);
connect
调用中的第一组括号采用两个函数参数:mapStateToProps
和mapDispatchToProps
。我们先定义mapStateToProps
,它以state
为参数。这state
是包含所有数据的全局 Redux 状态对象。函数返回我们希望在组件中使用的state
片段的对象。在我们的例子中,我们只需要photos
减速器的loadedPhotos
属性。通过在返回对象中将该值设置为photos
,我们可以期望this.props.photos
是存储在state.photos.loadedPhotos
中的值。当 Redux 存储更新时,它将自动更改:
class Album extends Component {
}
const mapStateToProps = (state) => {
return {
photos: state.photos.loadedPhotos
}
}
export default connect(mapStateToProps)(Album);
- 类似地,
mapDispatchToProps
函数也会将动作创建者映射到组件的道具。函数接收 Redux 方法dispatch
,该方法用于执行动作创建者。我们将每个动作创建者的执行映射到一个同名的键,这样this.props.fetchPhotos()
将执行dispatch(fetchPhotos())
,依此类推,如下所示:
class Album extends Component {
}
const mapStateToProps = (state) => {
return {
photos: state.photos.loadedPhotos
}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchPhotos: () => dispatch(fetchPhotos()),
addPhoto: (photo) => dispatch(addPhoto(photo)),
removePhoto: (id) => dispatch(removePhoto(id))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Album);
- 现在我们已经将 Redux 存储连接到组件,让我们创建组件本身。我们可以使用
componentDidMount
生命周期挂钩获取我们的照片,如下所示:
class Album extends Component {
componentDidMount() {
this.props.fetchPhotos();
}
// Defined on later steps
}
- 我们还需要一个添加照片的方法。在这里,我们将使用
randomcolor
包(按照约定导入为randomColor
来创建具有placehold.it服务的映像。生成的颜色字符串返回时带有一个以十六进制值为前缀的哈希值,这是对图像服务的请求所不需要的,因此我们可以通过一个replace
调用将其删除。为了添加照片,我们只需调用映射到props
的addPhoto
函数,传入新的photo
对象,如下所示:
addPhoto = () => {
const photo = {
"albumId": 2,
"title": "dolore esse a in eos sed",
"url": `http://placehold.it/600/${randomColor().replace('#',
'')}`,
"thumbnailUrl":
`http://placehold.it/150/${randomColor().replace('#', '')}`
};
this.props.addPhoto(photo);
}
- 我们还需要一个
removePhoto
函数。此函数只需调用已映射到props
的removePhoto
函数,传入要删除照片的 ID,如下所示:
removePhoto = (id) => {
this.props.removePhoto(id);
}
- 该应用的模板需要一个
TouchableOpacity
按钮来添加照片,一个ScrollView
按钮来保存可滚动列表中的所有图像,以及我们的所有图像。每个Image
组件也将被包装在TouchableOpacity
组件中,用于在按下图像时调用removePhoto
方法,如下所示:
render() {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.toolbar}>Album</Text>
<ScrollView>
<View style={styles.imageContainer}>
<TouchableOpacity style={styles.button} onPress=
{this.addPhoto}>
<Text style={styles.buttonText}>Add Photo</Text>
</TouchableOpacity>
{this.props.photos ? this.props.photos.map((photo) => {
return(
<TouchableOpacity onPress={() =>
this.removePhoto(photo.id)} key={Math.random()}>
<Image style={styles.image}
source={{ uri: photo.url }}
/>
</TouchableOpacity>
);
}) : null}
</View>
</ScrollView>
</SafeAreaView>
);
}
- 最后,我们将添加样式,以便应用具有布局,如下所示。这里没有我们以前多次讨论过的内容:
const styles = StyleSheet.create({
container: {
backgroundColor: '#ecf0f1',
flex: 1,
},
toolbar: {
backgroundColor: '#3498db',
color: '#fff',
fontSize: 20,
textAlign: 'center',
padding: 20,
},
imageContainer: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
},
image: {
height: 300,
width: 300
},
button: {
margin: 10,
padding: 20,
backgroundColor: '#3498db'
},
buttonText: {
fontSize: 18,
color: '#fff'
}
});
- 应用已完成!单击 Add Photo(添加照片)按钮将在图像列表的开头添加一张新照片,按下图像将其删除。注意,由于我们使用的是一个虚拟数据 API,
POST
和DELETE
请求将返回给定操作的正确响应。但是,实际上没有向数据库添加或删除任何数据。这意味着如果应用被刷新,图像列表将被重置,如果您试图删除您刚刚使用“添加照片”按钮添加的任何照片,则可能会出现错误。请随时将此应用连接到真实的 API 和数据库,以查看预期结果:
在步骤 4中,我们使用react-redux
提供的connect
方法授权Album
组件连接到我们一直在研究的 Redux 存储。对connect
的调用返回一个函数,该函数通过第二组括号立即执行。通过将Album
组件传递到此返回函数,connect
将组件和存储粘在一起。
在步骤 5中,我们定义了mapStateToProps
函数。此函数中的第一个参数是来自 Redux 存储的state
,由connect
注入函数。从mapStateToProps
返回的对象中定义的任何键都将是组件props
上的属性。这些道具的价值将在 Redux 商店中订阅state
,因此影响state
这些道具的任何更改都将在组件内自动更新
当mapStateToProps
将 Redux 商店中的state
映射到组件道具时,mapDispatchToProps
将动作创建者映射到组件道具。在步骤 6中,我们定义了这个函数。它有一个特殊的 Redux 方法,dispatch
被注入其中,用于调用存储中的动作创建者。mapDispatchToProps
返回一个对象,将dispatch
动作调用映射到指定键处的组件道具。
在步骤 7中,我们创建了componentDidMount
方法。组件在安装时需要做的就是调用映射到this.props.fetchPhotos
的 action creator 来获取所需的照片。这就是全部!fetchPhotos
动作创建者将被分派。从动作创建者返回的fetchPhoto
动作将由我们在前面的配方中应用的redux-promise-middleware
处理,因为此动作的payload
属性以axios
AJAX 请求的形式存储了一个承诺。中间件将拦截该操作,处理该请求,并使用payload
属性上的已解析数据向还原程序发送新操作。如果是成功的请求,FETCH_PHOTOS_FULFILLED
类型的动作将与已解析的数据一起调度,如果不是,则FETCH_PHOTOS_REJECTED
动作将与错误一起调度为payload
。成功后,执行FETCH_PHOTOS_FULFILLED
处理减速器中的案例,loadedPhotos
将在存储中更新,反过来,this.props.photos
也将更新。更新组件道具将触发重新渲染,新数据将显示在屏幕上。
在步骤 8和步骤 9中,我们按照相同的模式定义了addPhoto
和removePhoto
,它们调用了同名的动作创建者。动作创建者产生的动作由中间件处理,相应的减缩器处理产生的动作,如果 Redux store 中的state
发生变化,所有订阅的道具将自动更新!
Redux 是一个极好的工具,可以在应用运行时跟踪应用的状态。但如果我们有数据需要存储而不使用 API 呢?例如,我们可以保存组件的状态,以便当用户关闭并重新打开应用时,该组件的先前状态可以恢复,从而允许我们在会话间持久化应用的一部分。Redux 数据持久性还可以用于缓存信息,以避免调用 API 的次数过多。有关如何检测和处理网络连接状态的更多信息,请参考第 8 章中的在网络连接丢失时屏蔽应用配方,使用应用逻辑和数据。
此食谱取决于之前的食谱,因此请确保遵循之前的所有食谱。在此配方中,我们将使用redux-persist
包将数据持久化到应用的 Redux 存储中。用npm
安装:
npm install --save redux-persist
也可以使用yarn
进行安装:
yarn add redux-persist
- 让我们从添加
redux/index.js
中需要的依赖项开始。这里我们从redux-persist
导入的storage
方法将使用 React Native 的AsyncStorage
方法在会话之间存储 Redux 数据,如下所示:
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage';
- 我们将使用一个简单的
config
对象来配置redux-persist
实例。config
需要一个key
属性,用于将数据存储到AsyncStore
的密钥,以及一个以storage
实例为例的存储属性,如下所示:
const persistConfig = {
key: 'root',
storage
}
- 我们将使用我们在步骤 1中导入的
persistReducer
方法。此方法将我们在步骤 2中创建的config
对象作为第一个参数,我们的约简器作为第二个参数:
const reducers = combineReducers({
photos,
});
const persistedReducer = persistReducer(persistConfig, reducers);
- 现在让我们更新我们的商店以使用新的
persistedReducer
方法。还要注意我们如何不再将store
导出为默认导出,因为我们需要从该文件导出两个导出:
export const store = createStore(persistedReducer, applyMiddleware(promiseMiddleware()));
- 我们需要从该文件中导出的第二个文件是
persistor
。persistor
将在会话之间保持 Redux 存储。我们可以通过调用persistStore
方法并传入store
来创建persistor
,如下所示:
export const persistor = persistStore(store);
- 现在我们已经将
store
和persistor
作为redux/index.js
的出口,我们准备将它们应用于App.js
。我们将首先导入它们,然后从redux-persist
导入PersistGate
组件。PersistGate
将确保在加载任何组件之前加载缓存的 Redux 存储:
import { PersistGate } from 'redux-persist/integration/react'
import { store, persistor } from './redux';
- 让我们更新
App
组件以使用PersistGate
。该组件有两个道具:进口的persistor
道具和一个loading
道具。我们将把null
传递给loading
道具,但如果我们有一个加载指示器组件,我们可以将其传递进来,PersistGate
将在数据恢复时显示此加载指示器,如下所示:
const App = () => (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Album />
</PersistGate>
</Provider>
);
- 为了测试 Redux 存储的持久性,让我们调整
Album
组件中的componentDidMount
方法。我们会将对fetchPhotos
的调用延迟两秒钟,以便在再次从 API 获取数据之前可以看到保存的数据,如下所示:
componentDidMount() {
setTimeout(() => {
this.props.fetchPhotos();
}, 2000);
}
根据要持久化的数据类型,这种功能可以应用于多种情况,包括持久化用户数据和应用状态,即使在应用关闭后也是如此。它还可以用来改善应用的离线体验,在 API 请求不能立即发出时缓存它们,并为用户提供数据填充视图。
在步骤 2中,我们创建了配置redux-persist
的配置对象。该对象只需要具有key
和store
属性,还需要支持很多其他属性。您可以通过此处托管的类型定义看到此配置所采用的所有选项:https://github.com/rt2zz/redux-persist/blob/master/src/types.js#L13-L27
在步骤 7中,我们使用了PersistGate
组件,文档建议延迟渲染,直到恢复持久数据完成。如果我们有一个加载指示器组件,我们可以将其传递给loading
道具,以便在数据恢复时显示。