Sådan indlæses data i Reager med redux-thunk, redux-saga, spænding og kroge

Introduktion

React er et JavaScript-bibliotek til opbygning af brugergrænseflader. Meget ofte at bruge React betyder at bruge React with Redux. Redux er et andet JavaScript-bibliotek til styring af global tilstand. Desværre er der selv med disse to biblioteker ingen klar måde at håndtere asynkrone opkald til API (backend) eller andre bivirkninger.

I denne artikel forsøger jeg at sammenligne forskellige tilgange til løsning af dette problem. Lad os først definere problemet.

Komponent X er en af ​​de mange komponenter på webstedet (eller mobil- eller desktop-applikation, det er også muligt). X spørger og viser nogle data indlæst fra API'en. X kan være side eller bare en del af siden. Vigtigt, at X er en separat komponent, som skal løses sammen med resten af ​​systemet (så meget som muligt). X skal vise indlæsningsindikator, mens data hentes og fejl, hvis opkald mislykkes.

Denne artikel antager, at du allerede har en vis erfaring med oprettelse af React / Redux-applikationer.

Denne artikel vil vise 4 måder at løse dette problem på og sammenligne fordele og ulemper ved hver enkelt. Det er ikke en detaljeret manual om, hvordan man bruger tunk, saga, suspension eller kroge .

Koden til disse eksempler er tilgængelig på GitHub.

Første opsætning

Mock Server

Til testformål skal vi bruge json-server. Det er et fantastisk projekt, der giver dig mulighed for at oprette falske REST API'er meget hurtigt. For vores eksempel ser det sådan ud.

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...`);});

Vores db.json-fil indeholder testdata i json-format.

{ "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 } ]}

Efter start af serveren returnerer et opkald til // localhost: 4000 / brugere listen over brugere med en efterligning af forsinkelse - ca. 2 sek.

Projekt- og API-opkald

Nu er vi klar til at starte kodning. Jeg antager, at du allerede har et React-projekt oprettet ved hjælp af create-react-app med Redux konfigureret og klar til brug.

Hvis du har problemer med det, kan du tjekke dette og dette.

Det næste trin er at oprette en funktion til at kalde API ( api.js ):

const API_BASE_ADDRESS = '//localhost:4000';
export default class Api { static getUsers() { const uri = API_BASE_ADDRESS + "/users";
 return fetch(uri, { method: 'GET' }); }}

Redux-thunk

Redux-thunk er en anbefalet middleware til grundlæggende Redux-bivirkningslogik, såsom simpel async-logik (som en anmodning til API). Redux-thunk selv gør ikke meget. Det er bare 14 !!! linjer i koden. Det tilføjer bare noget “syntaks sukker” og intet mere.

Flowdiagrammet nedenfor hjælper med at forstå, hvad vi skal gøre.

Hver gang en handling udføres, skifter reduceringsenheden tilstand i overensstemmelse hermed. Komponenten kortlægger tilstand til egenskaber og bruger disse egenskaber i revder () -metoden til at finde ud af, hvad brugeren skal se: en indlæsningsindikator, data eller fejlmeddelelse.

For at få det til at fungere, er vi nødt til at gøre 5 ting.

1. Installer tunk

npm install redux-thunk

2. Tilføj thunk middleware, når du konfigurerer butik (configureStore.js)

import { applyMiddleware, compose, createStore } from 'redux';import thunk from 'redux-thunk';import rootReducer from './appReducers';
export function configureStore(initialState) 

I linje 12-13 konfigurerer vi også redux devtools. Lidt senere hjælper det med at vise et af problemerne med denne løsning.

3. Opret handlinger (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() )};

Det anbefales også at have dine handlingsskabere adskilt (det tilføjer yderligere kodning), men i denne enkle sag synes jeg det er acceptabelt at oprette handlinger "på farten".

4. Opret reducer (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. Opret komponent tilsluttet 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 
   
ERROR: {this.props.error} }
 return ( 
    
       {this.props.data.map(u =>
      <;td>{u.posts}
        )} 
     
First Name Last Name;Active? Posts Messages
{u.firstName} {u.lastName} {u.active ? 'Yes' : 'No'}{u.messages}
); }}
const mapStateToProps = state => ({ data: state.reduxThunk.data, loading: state.reduxThunk.loading, error: state.reduxThunk.error,});
const mapDispatchToProps = { loadUsers};
export default connect( mapStateToProps, mapDispatchToProps)(UsersWithReduxThunk);

Jeg forsøgte at gøre komponenten så enkel som muligt. Jeg forstår, at det ser forfærdeligt ud :)

Indlæsningsindikator

Data

Fejl

Der har du det: 3 filer, 109 kodelinje (13 (handlinger) + 36 (reducer) + 60 (komponent)).

Fordele:

  • "Anbefalet" tilgang til reaktor / redux applikationer.
  • Ingen yderligere afhængigheder. Næsten, thunk er lille :)
  • Ingen grund til at lære nye ting.

Ulemper:

  • En masse kode forskellige steder
  • After navigation to another page, old data is still in the global state (see picture below). This data is outdated and useless information that consumes memory.
  • In case of complex scenarios (multiple conditional calls in one action, etc.) code isn’t very readable

Redux-saga

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.

From a high level perspective, this solution works the same as thunk. The flowchart from the thunk example is still applicable.

To make it work we need to do 6 things.

1. Install saga

npm install redux-saga

2. Add saga middleware and add all 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) 

Sagas from line 4 will be added in step 4.

3. Create action (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. Create 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 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 fetchUsersfunction. The fetchUsersfunction calls the API. If the call succeeds, then the LOAD_USER_SUCCESS action is dispatched, otherwise the LOAD_USER_ERROR action is dispatched.

5. Create 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; } }}

The reducer here is absolutely the same as in the thunk example.

6. Create component connected to 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 
   
ERROR: {this.props.error} }
 return ( 
    ; 
     
       {this.props.data.map(u =>
       )} 
     
First Name Last Name;Active? Posts Messages
{u.firstName} ;{u.lastName} {u.active ? 'Yes' : 'No'} {u.posts} {u.messages}
); }}
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.

So here we have 4 files, 136 line of code (7(actions) + 36(reducer) + sagas(33) + 60(component)).

Pros:

  • More readable code (async/await)
  • Good for handling complex scenarios (multiple conditional calls in one action, action can have multiple listeners, canceling actions, etc.)
  • Easy to unit test

Cons:

  • A lot of code in different places
  • After navigation to another page, old data is still in the global state. This data is outdated and useless information that consumes memory.
  • Additional dependency
  • A lot of concepts to learn

Suspense

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).

To make it work we need to do 4 things (it’s definitely getting better :) ).

1. Create cache (suspense/cache.js)

For the cache, we are going to use a simple-cache-provider which is a basic cache provider for react applications.

import {createCache} from 'simple-cache-provider';
export let cache;
function initCache() { cache = createCache(initCache);}
initCache();

2. Create Error Boundary (suspense/ErrorBoundary.js)

This is an Error Boundary to catch errors thrown by Suspense.

import React from 'react';
export class ErrorBoundary extends React.Component { state = {};
 componentDidCatch(error) { this.setState(); }
 render() { if (this.state.error) { return 
   
ERROR: this.state.error ; }
 return this.props.children; }}
export default ErrorBoundary;

3. Create Users Table (suspense/UsersTable.js)

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.

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 ( 
    <;td>{u.posts}
        )} 
     
First Name ;Last Name Active? Posts Messages
{u.firstName} {u.lastName} {u.active ? 'Yes' : 'No'}{u.messages}
); }}
export default UsersTable;

4. Create component (suspense/UsersWithSuspense.js)

import * as React from 'react';import UsersTable from "./UsersTable";import ErrorBoundary from "./ErrorBoundary";
class UsersWithSuspense extends React.Component { render() { return ( 
    
     
       ); }}
     
    
export default UsersWithSuspense;

4 files, 106 line of code (9(cache) + 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.

Pros:

  • No redux needed. This approach can be used without redux. Component is fully independent.
  • No additional dependencies (simple-cache-provider is part of React)
  • Delay of showing Loading indicator by setting dellayMs property
  • Fewer lines of code than in previous examples

Cons:

  • Cache is needed even when we don’t really need caching.
  • Some new concepts need to be learned (which are part of React).

Hooks

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.

To make it work for our example we need to do one(!) thing:

1. Create and use hooks (hooks/UsersWithHooks.js)

Here we are creating 3 hooks (functions) to “hook into” React state.

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)  'Unexpected error'); 
 setLoading(false); }, []);
 if (loading) { return 
   
Loading }
 if (error) { return 
   
ERROR: {error} }
 return ( 
    
       ; 
       
       {data.map(u => 
      
       ; 
       ; 
        )} 
     
First Name Last Name Active? PostsMessages
;{u.firstName}{u.lastName} {u.active ? 'Yes' : 'No'} {u.posts} {u.messages}
);}
export default UsersWithHooks;

And that’s it — just 1 file, 56 line of code!!!

Fordele:

  • Ingen redux nødvendig. Denne tilgang kan bruges uden redux. Komponenten er fuldstændig uafhængig.
  • Ingen yderligere afhængigheder
  • Cirka 2 gange mindre kode end i andre løsninger

Ulemper:

  • Ved første kig ser koden underlig og vanskelig at læse og forstå. Det vil tage lidt tid at vænne sig til kroge.
  • Nogle nye begreber skal læres (som er en del af React)
  • Ikke officielt frigivet endnu

Konklusion

Lad os organisere disse metrics som en tabel først.

  • Redux er stadig en god mulighed for at styre den globale stat (hvis du har det)
  • Hver mulighed har fordele og ulemper. Hvilken tilgang, der er bedre, afhænger af projektet: dets kompleksitet, use cases, team-viden, hvornår projektet skal produktion osv.
  • Saga kan hjælpe med komplekse brugssager
  • Spænding og kroge er begge værd at overveje (eller i det mindste lære) især til nye projekter

Det er det - nyd og glad kodning!