by Valerii Tereshchenko
瓦莱里·捷列申科(Valerii Tereshchenko)
React is a JavaScript library for building user interfaces. Very often using React means using React with Redux. Redux is another JavaScript library for managing global state. Sadly, even with these two libraries there is no one clear way how to handle asynchronous calls to the API (backend) or any other side effects.
React是一个用于构建用户界面JavaScript库。 通常,使用React意味着将React与Redux结合使用 。 Redux是另一个用于管理全局状态JavaScript库。 遗憾的是,即使有这两个库,也没有一种明确的方法来处理对API(后端)的异步调用或任何其他副作用。
In this article I’m trying to compare different approaches to solving this problem. Let’s define the problem first.
在本文中,我试图比较解决此问题的不同方法。 让我们先定义问题。
Component X is one of the many components of the web site (or mobile, or desktop application, it’s also possible). X queries and shows some data loaded from the API. X can be page or just part of the page. Important thing that X is a separate component which should be loosely coupled with the rest of the system (as much as possible). X should show loading indicator while data is retrieving and error if call fails.
组件X是网站的许多组件之一(也可以是移动或桌面应用程序)。 X查询并显示一些从API加载的数据。 X可以是页面,也可以只是页面的一部分。 重要的是X是一个独立的组件,应与系统的其余部分(尽可能多)松散耦合。 X在数据检索时应显示加载指示符,如果调用失败,则应显示错误。
This article assumes that you already have some experience with creating React/Redux applications.
本文假设您已经具有创建React / Redux应用程序的经验。
This article is going to show 4 ways of solving this problem and compare pros and cons of each one. It isn’t a detailed manual on how to use thunk, saga, suspence or hooks.
本文将展示解决此问题的4种方法,并比较每种方法的优缺点 。 它不是有关如何使用thunk,saga,suspence或hooks的详细手册 。
Code of these examples is available on GitHub.
这些示例的代码可在GitHub上获得 。
For testing purposes we are going to use json-server. It’s an amazing project that allows you to build fake REST APIs very fast. For our example, it looks like this.
为了进行测试,我们将使用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...`);});
Our db.json file contains test data in json format.
我们的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 } ]}
After starting the server, a call to the http://localhost:4000/users returns the list of the users with an imitation of delay — about 2s.
启动服务器后,对http:// localhost:4000 / users的调用会返回用户列表,并带有延迟(大约2秒)。
Now we are ready to start coding. I assume that you already have a React project created using create-react-app with Redux configured and ready to use.
现在我们准备开始编码。 我假设您已经有一个使用create-react-app创建的React项目,并且已配置Redux并准备使用它。
If you have any difficulties with it you can check out this and this.
The next step is to create a function to call the API (api.js):
下一步是创建一个函数来调用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 is a recommended middleware for basic Redux side effects logic, such as simple async logic (like a request to the API). Redux-thunk itself doesn’t do a lot. It’s just 14!!! lines of the code. It just adds some “syntax sugar” and nothing more.
建议使用Redux-thunk作为基本Redux副作用逻辑的中间件,例如简单的异步逻辑(例如对API的请求)。 Redux-thunk本身并没有做什么。 才14点!!! 行 的 所述 代码 。 它只是添加了一些“语法糖”,仅此而已。
The flowchart below helps to understand what we are going to do.
下面的流程图有助于了解我们将要做什么。
Every time an action is performed, the reducer changes state accordingly. The component maps state to properties and uses these properties in the revder() method to figure out what the user should see: a loading indicator, data or error message.
每次执行操作时,减速器都会相应地更改状态。 该组件将状态映射到属性,并在revder()方法中使用这些属性来弄清楚用户应该看到的内容:加载指示符,数据或错误消息。
To make it work we need to do 5 things.
要使其工作,我们需要做5件事。
npm install redux-thunk
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;}
In lines 12–13 we also configure redux devtools. A bit later it will help to show one of the problems with this solution.
在第12-13行中,我们还配置了redux devtools 。 稍后,它将有助于显示此解决方案的问题之一。
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!!!' }) )};
It’s also recommended to have your action creators separated (it adds some additional coding), but for this simple case I think it’s acceptable to create actions “on the fly”.
还建议将动作创建者分开(这会增加一些额外的编码),但是对于这种简单情况,我认为“即时”创建动作是可以接受的。
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; } }}
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 <div>Loading</div> }
if (this.props.error) { return <div style={{ color: 'red' }}>ERROR: {this.props.error}</div> }
return ( <table> <thead> <tr> <th>First Name</th> <th>Last Name</th> <th>;Active?</th> <th>Posts</th> <th>Messages</th> </tr> </thead> <tbody> {this.props.data.map(u => <tr key={u.id}> <td>{u.firstName}</td> <td>{u.lastName}</td> <td>{u.active ? 'Yes' : 'No'}</td> <;td>{u.posts}</td> <td>{u.messages}</td> </tr> )} </tbody> </table> ); }}
const mapStateToProps = state => ({ data: state.reduxThunk.data, loading: state.reduxThunk.loading, error: state.reduxThunk.error,});
const mapDispatchToProps = { loadUsers};
export default connect( mapStateToProps, mapDispatchToProps)(UsersWithReduxThunk);
I tried to make the component as simple as possible. I understand that it looks awful :)
我试图使组件尽可能简单。 我了解它看起来很糟糕:)
Loading indicator
加载指示器
Data
数据
Error
错误
There you have it: 3 files, 109 line of code (13(actions) + 36(reducer) + 60(component)).
那里有:3个文件,109行代码(13(动作)+ 36(归约器)+ 60(组件))。
Redux-saga is a redux middleware library designed to make handling side effects easy and readable. It leverages ES6 Generators which allows us to write asynchronous code that looks synchronous. Also, this solution is easy to test.
Redux-saga是一个Redux中间件库,旨在使处理副作用变得容易且可读。 它利用ES6生成器,使我们能够编写看起来是同步的异步代码。 而且,此解决方案易于测试。
From a high level perspective, this solution works the same as thunk. The flowchart from the thunk example is still applicable.
从高级的角度来看,此解决方案与thunk相同。 thunk示例中的流程图仍然适用。
To make it work we need to do 6 things.
要使其工作,我们需要做6件事。
npm install redux-saga
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;}
Sagas from line 4 will be added in step 4.
来自第4行的Sagas将在步骤4中添加。
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 });};
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 has quite a steep learning curve, so if you’ve never used it and have never read anything about this framework it could be difficult to understand what’s going on here. Briefly, in the userSaga function we configure saga to listen to the LOAD_USERS_LOADING action and trigger the fetchUsers function. The fetchUsers function calls the API. If the call succeeds, then the LOAD_USER_SUCCESS action is dispatched, otherwise the LOAD_USER_ERROR action is dispatched.
Saga的学习曲线相当陡峭,因此,如果您从未使用过它,并且从未阅读过有关该框架的任何内容,那么可能很难理解这里发生的事情。 简要地说,在userSaga函数中,我们将saga配置为侦听LOAD_USERS_LOADING操作并触发fetchUsers函数。 fetchUsers函数调用API。 如果调用成功,则调度LOAD_USER_SUCCESS操作,否则调度LOAD_USER_ERROR操作。
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; } }}
The reducer here is absolutely the same as in the thunk example.
这里的reducer与thunk示例完全相同。
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 <div>Loading</div> }
if (this.props.error) { return <div style={{color: 'red'}}>ERROR: {this.props.error}</div> }
return ( <table> <thead> <tr> <th>First Name</th> <th>Last Name</th> <th>;Active?</th> <th>Posts</th> <th>Messages</th> </tr>; </thead> <tbody> {this.props.data.map(u => <tr key={u.id}> <td>{u.firstName}</td> <td>;{u.lastName}</td> <td>{u.active ? 'Yes' : 'No'}</td> <td>{u.posts}</td> <td>{u.messages}</td> </tr> )} </tbody> </table> ); }}
const mapStateToProps = state => ({ data: state.reduxSaga.data, loading: state.reduxSaga.loading, error: state.reduxSaga.error,});
const mapDispatchToProps = { loadUsers};
export default connect( mapStateToProps, mapDispatchToProps)(UsersWithReduxSaga);
The component is also almost the same here as in the thunk example.
这里的组件也与thunk示例中的组件几乎相同。
So here we have 4 files, 136 line of code (7(actions) + 36(reducer) + sagas(33) + 60(component)).
因此,这里有4个文件,136行代码(7(动作)+ 36(减速器)+ sagas(33)+ 60(组件))。
Suspense is a new feature in React 16.6.0. It allows us to defer rendering part of the component until some condition is met (for example data from the API loaded).
Suspense是React 16.6.0中的新功能。 它使我们可以推迟渲染组件的一部分,直到满足某些条件(例如,从API加载的数据)为止。
To make it work we need to do 4 things (it’s definitely getting better :) ).
为了使其正常工作,我们需要做4件事(它肯定会变得越来越好:))。
For the cache, we are going to use a simple-cache-provider which is a basic cache provider for react applications.
对于缓存,我们将使用简单缓存提供程序 ,它是React应用程序的基本缓存提供程序。
import {createCache} from 'simple-cache-provider';
export let cache;
function initCache() { cache = createCache(initCache);}
initCache();
This is an Error Boundary to catch errors thrown by Suspense.
这是一个错误边界,用于捕获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 <div style={{ color: 'red' }}>ERROR: {this.state.error || 'Unexpected Error'}</div>; }
return this.props.children; }}
export default ErrorBoundary;
For this example, we need to create an additional component which loads and shows data. Here we are creating a resource to get data from the API.
对于此示例,我们需要创建一个额外的组件来加载和显示数据。 在这里,我们正在创建资源以从API获取数据。
import * as React from 'react';import {createResource} from "simple-cache-provider";import {cache} from "./cache";import Api from "../api";
let UsersResource = createResource(async () => { const response = await Api.getUsers(); const json = await response.json();
return json;});
class UsersTable extends React.Component { render() { let users = UsersResource.read(cache);
return ( <table> <thead> <tr> <th>First Name</th> <th>;Last Name</th> <th>Active?</th> <th>Posts</th> <th>Messages</th> </tr> </thead> <tbody> {users.map(u => <tr key={u.id}> <td>{u.firstName}</td> <td>{u.lastName}</td> <td>{u.active ? 'Yes' : 'No'}</td> <;td>{u.posts}</td> <td>{u.messages}</td> </tr> )} </tbody> </table> ); }}
export default UsersTable;
import * as React from 'react';import UsersTable from "./UsersTable";import ErrorBoundary from "./ErrorBoundary";
class UsersWithSuspense extends React.Component { render() { return ( <ErrorBoundary> <React.Suspense fallback={<div>Loading</div>}> <UsersTable/> </React.Suspense> </ErrorBoundary> ); }}
export default UsersWithSuspense;
4 files, 106 line of code (9(cache) + 19(ErrorBoundary) + UsersTable(33) + 45(component)).
4个文件,106行代码(9(缓存)+ 19(ErrorBoundary)+ UsersTable(33)+ 45(component))。
3 files, 87 line of code (9(cache) + UsersTable(33) + 45(component)) if we assume that ErrorBoundary is a reusable component.
如果我们假定ErrorBoundary是可重用的组件,则3个文件,87行代码(9(缓存)+ UsersTable(33)+ 45(组件))。
No additional dependencies (simple-cache-provider is part of React)
没有额外的依赖关系( simple-cache-provider是React的一部分)
At the time of writing this article, hooks have not officially been released yet and available only in the “next” version. Hooks are indisputably one of the most revolutionary upcoming features which can change a lot in the React world very soon. More details about hooks can be found here and here.
在撰写本文时,挂钩尚未正式发布,仅在“下一个”版本中可用。 毫无疑问,挂钩是即将来临的最具革命性的功能之一,它很快就会在React世界中发生很大的变化。 有关钩子的更多详细信息,请参见此处和此处 。
To make it work for our example we need to do one(!) thing:
为了使其适用于我们的示例,我们需要做一个(!)事情:
Here we are creating 3 hooks (functions) to “hook into” React state.
在这里,我们将创建3个挂钩(函数)以“挂钩”到React状态。
import React, {useState, useEffect} from 'react';import Api from "../api";
function UsersWithHooks() { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState('');
useEffect(async () => { try { const response = await Api.getUsers(); const json = await response.json();
setData(json); } catch (e) { setError(e.message || 'Unexpected error'); }
setLoading(false); }, []);
if (loading) { return <div>Loading</div> }
if (error) { return <div style={{color: 'red'}}>ERROR: {error}</div> }
return ( <table> <thead> <tr>; <th>First Name</th> <th>Last Name</th> <th>Active?</th> <th>Posts</th> <th>Messages</th> </tr> </thead> <tbody> {data.map(u => <tr key={u.id}>; <td>;{u.firstName}</td>; <td>{u.lastName}</td> <td>{u.active ? 'Yes' : 'No'}<;/td> <td>{u.posts}</td> <td>{u.messages}</td> </tr> )} </tbody> </table> );}
export default UsersWithHooks;
And that’s it — just 1 file, 56 line of code!!!
就是这样-仅1个文件,共56行代码!!!
Let’s organize these metrics as a table first.
首先让我们将这些指标组织为表格。
That’s it — enjoy and happy coding!
就是这样-享受愉快的编码!