目录
介绍
初始设置
模拟服务器
项目和API调用
Redux-thunk
Redux-saga
Suspense
Hooks
结论
介绍React 是一个用于构建用户界面的JavaScript库。经常使用React意味着将React与Redux一起使用。Redux 是另一个用于管理全局状态的JavaScript库。遗憾的是,即使有了这两个库,也没有一种明确的方法来处理对API(后端)的异步调用或任何其他的副作用。
在本文中,我试图比较解决此问题的不同方法。我们先来定义问题。
组件X是网站的许多组件之一(或移动或桌面应用程序,它也是可能的)。X查询并显示从API加载的一些数据。X可以是页面,也可以只是页面的一部分。重要的是X是一个独立的组件,应该与系统的其余部分松散地耦合(尽可能多)。X应该在数据检索时显示加载指示符,如果调用失败则显示错误。
本文假设您已经具备了创建React / Redux应用程序的一些经验。
本文将展示解决这个问题的4种方法,并比较每种方法的优缺点。没有详细的使用说明如何使用thunk,saga,suspence或hooks。
GitHub上提供了这些示例的代码。
初始设置 模拟服务器出于测试目的,我们将使用json - server。这个令人惊叹的项目允许非常快速地构建虚假的REST API。对于我们的示例,它看起来像这样。
const jsonServer = require('json-server');
const server = jsonServer.create();
const router = jsonServer.router('db.json');
const middleware = jsonServer.defaults();
server.use((req, res, next) => {
setTimeout(() => next(), 2000);
});
server.use(middleware);
server.use(router);
server.listen(4000, () => {
console.log(`JSON Server is running...`);
});
db.json文件包含json格式的测试数据。
{
"users": [
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"active": true,
"posts": 10,
"messages": 50
},
...
{
"id": 8,
"firstName": "Clay",
"lastName": "Chung",
"active": true,
"posts": 8,
"messages": 5
}
]
}
启动服务器后,调用http://localhost:4000/users返回模仿延迟大约2s的用户列表。
项目和API调用现在我们准备开始编码了。我假设您已经使用create - react - app 创建了 React项目,并配置了Redux并准备使用。
如果您遇到任何困难,可以查看这里和本文。
接下来是创建一个调用API的函数(api.js)
const API_BASE_ADDRESS = 'http://localhost:4000';
export default class Api {
static getUsers() {
const uri = API_BASE_ADDRESS + "/users";
return fetch(uri, {
method: 'GET'
});
}
}
Redux-thunk
Redux - thunk是推荐的用于基本Redux副作用逻辑的中间件,比如简单的异步逻辑,比如对API的请求。Redux-thunk本身并没有做太多。这只是14!行的的代码。它只是添加了一些“语法糖”,仅此而已。
下面的流程图有助于了解我们将要做什么。
每次执行动作时,减速器都会相应地改变状态。组件将状态映射到属性,并在revder()方法中使用这些属性来确定用户应该看到的内容:加载指示符,数据或错误消息。
为了使它工作,我们需要做5件事。
1.安装tunk
npm install redux-thunk
2.配置存储时添加thunk中间件(configureStore.js)
import { applyMiddleware, compose, createStore } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './appReducers';
export function configureStore(initialState) {
const middleware = [thunk];
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, initialState, composeEnhancers(applyMiddleware(...middleware)));
return store;
}
在第12-13行,我们还配置了redux devtools。稍后,它将有助于显示此解决方案的一个问题。
3.创建动作(redux-thunk / actions.js)
import Api from "../api"
export const LOAD_USERS_LOADING = 'REDUX_THUNK_LOAD_USERS_LOADING';
export const LOAD_USERS_SUCCESS = 'REDUX_THUNK_LOAD_USERS_SUCCESS';
export const LOAD_USERS_ERROR = 'REDUX_THUNK_LOAD_USERS_ERROR';
export const loadUsers = () => dispatch => {
dispatch({ type: LOAD_USERS_LOADING });
Api.getUsers()
.then(response => response.json())
.then(
data => dispatch({ type: LOAD_USERS_SUCCESS, data }),
error => dispatch({ type: LOAD_USERS_ERROR, error: error.message || 'Unexpected Error!!!' })
)
};
它还建议将动作创建者分开(它增加了一些额外的编码),但对于这个简单的情况,我认为“动态”创建动作是可以接受的。
4.创建reduser(redux-thunk / reducer.js)
import {LOAD_USERS_ERROR, LOAD_USERS_LOADING, LOAD_USERS_SUCCESS} from "./actions";
const initialState = {
data: [],
loading: false,
error: ''
};
export default function reduxThunkReducer(state = initialState, action) {
switch (action.type) {
case LOAD_USERS_LOADING: {
return {
...state,
loading: true,
error:''
};
}
case LOAD_USERS_SUCCESS: {
return {
...state,
data: action.data,
loading: false
}
}
case LOAD_USERS_ERROR: {
return {
...state,
loading: false,
error: action.error
};
}
default: {
return state;
}
}
}
5.创建连接到redux的组件(redux-thunk / UsersWithReduxThunk.js)
import * as React from 'react';
import { connect } from 'react-redux';
import {loadUsers} from "./actions";
class UsersWithReduxThunk extends React.Component {
componentDidMount() {
this.props.loadUsers();
};
render() {
if (this.props.loading) {
return Loading
}
if (this.props.error) {
return { color: 'red' }}ERROR: {this.props.error}
我试图使组件尽可能简单。我明白它看起来很糟糕:)
加载指标
数据
错误
3个文件,109行代码(13(动作)+ 36(减速器)+ 60(组件))。
优点:
- react/redux 应用程序的“推荐”方法。
- 没有其他依赖项。差不多,thunk很小:)
- 无需学习新事物。
缺点:
- 很多代码在不同的地方
- 导航到另一页后,旧数据仍处于全局状态(见下图)。这些数据已经过时,而且消耗内存的无用信息也是如此。
- 在复杂场景(一个动作中的多个条件调用等)的情况下,代码不是非常易读
Redux - saga是一个redux中间件库,旨在以简单易读的方式处理副作用。它利用ES6 生成器,允许编写看起来同步的异步代码。此外,该解决方案易于测试。
从高层次的角度来看,这个解决方案与thunk一样。来自thunk示例的流程图仍然适用。
为了使它工作,我们需要做6件事。
1.安装saga
npm install redux-thunk
2.添加saga中间件并添加所有sagas(configureStore.js)
import { applyMiddleware, compose, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './appReducers';
import usersSaga from "../redux-saga/sagas";
const sagaMiddleware = createSagaMiddleware();
export function configureStore(initialState) {
const middleware = [sagaMiddleware];
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, initialState, composeEnhancers(applyMiddleware(...middleware)));
sagaMiddleware.run(usersSaga);
return store;
}
第4行的Sagas将在第4步中添加。
3.创建动作(redux-saga / actions.js)
export const LOAD_USERS_LOADING = 'REDUX_SAGA_LOAD_USERS_LOADING';
export const LOAD_USERS_SUCCESS = 'REDUX_SAGA_LOAD_USERS_SUCCESS';
export const LOAD_USERS_ERROR = 'REDUX_SAGA_LOAD_USERS_ERROR';
export const loadUsers = () => dispatch => {
dispatch({ type: LOAD_USERS_LOADING });
};
4.创造sagas(redux-saga / sagas.js)
import { put, takeEvery, takeLatest } from 'redux-saga/effects'
import {loadUsersSuccess, LOAD_USERS_ERROR, LOAD_USERS_LOADING, LOAD_USERS_SUCCESS} from "./actions";
import Api from '../api'
async function fetchAsync(func) {
const response = await func();
if (response.ok) {
return await response.json();
}
throw new Error("Unexpected error!!!");
}
function* fetchUser() {
try {
const users = yield fetchAsync(Api.getUsers);
yield put({type: LOAD_USERS_SUCCESS, data: users});
} catch (e) {
yield put({type: LOAD_USERS_ERROR, error: e.message});
}
}
export function* usersSaga() {
// Allows concurrent fetches of users
yield takeEvery(LOAD_USERS_LOADING, fetchUser);
// Does not allow concurrent fetches of users
// yield takeLatest(LOAD_USERS_LOADING, fetchUser);
}
export default usersSaga;
saga的学习曲线相当大,所以如果你从未使用过,也从未读过任何关于这个框架的内容,那么就很难理解她的情况。简而言之,在userSaga函数中,我们配置saga来监听LOAD_USERS_LOADING动作并触发fetchUsers 函数。fetchUsers 函数调用API。如果调用成功,则发送LOAD_USER_SUCCESS动作,否则发送LOAD_USER_ERROR动作。
5.创建reducer(redux-saga / reducer.js)
import {LOAD_USERS_ERROR, LOAD_USERS_LOADING, LOAD_USERS_SUCCESS} from "./actions";
const initialState = {
data: [],
loading: false,
error: ''
};
export default function reduxSagaReducer(state = initialState, action) {
switch (action.type) {
case LOAD_USERS_LOADING: {
return {
...state,
loading: true,
error:''
};
}
case LOAD_USERS_SUCCESS: {
return {
...state,
data: action.data,
loading: false
}
}
case LOAD_USERS_ERROR: {
return {
...state,
loading: false,
error: action.error
};
}
default: {
return state;
}
}
}
Reducer与thunk示例完全相同。
6.创建连接到redux的组件(redux-saga / UsersWithReduxSaga.js)
import * as React from 'react';
import {connect} from 'react-redux';
import {loadUsers} from "./actions";
class UsersWithReduxSaga extends React.Component {
componentDidMount() {
this.props.loadUsers();
};
render() {
if (this.props.loading) {
return Loading
}
if (this.props.error) {
return {color: 'red'}}ERROR: {this.props.error}
组件也与thunk示例中的组件几乎相同。
4个文件,136行代码(7(动作)+ 36(减速器)+sagas(33)+ 60(组件))。
优点:
- 更易读的代码(async/await)
- 适合处理复杂场景(一个动作中有多个条件调用,动作可以有多个侦听器,取消动作等)
- 易于单元测试
缺点:
- 很多代码在不同的地方
- 导航到另一个页面后,旧数据仍处于全局状态。这些数据已经过时,而且消耗内存的无用信息也是如此。
- 额外的依赖
- 要学习很多概念
Suspense是React 16.6.0中的新功能。它允许延迟渲染组件的一部分,直到满足某些条件(例如来自API加载的数据)。
为了使它工作,我们需要做4件事(它肯定会变得更好:))。
1.创建缓存(suspense / cache.js)
对于缓存,我们将使用simple - cache - provider,它是react应用程序的基本疼痛提供程序。
import {createCache} from 'simple-cache-provider';
export let cache;
function initCache() {
cache = createCache(initCache);
}
initCache();
2.创建错误边界(suspense / ErrorBoundary.js)
捕获Suspense引发的错误是一个错误边界。
import React from 'react';
export class ErrorBoundary extends React.Component {
state = {};
componentDidCatch(error) {
this.setState({ error: error.message || "Unexpected error" });
}
render() {
if (this.state.error) {
return { color: 'red' }}ERROR: {this.state.error || 'Unexpected Error'}