Πώς να φορτώσετε δεδομένα στο React με redux-thunk, redux-saga, suspense & hooks

Εισαγωγή

Το React είναι μια βιβλιοθήκη JavaScript για τη δημιουργία διεπαφών χρήστη. Πολύ συχνά η χρήση του React σημαίνει τη χρήση του React with Redux. Το Redux είναι μια άλλη βιβλιοθήκη JavaScript για τη διαχείριση της παγκόσμιας κατάστασης. Δυστυχώς, ακόμη και με αυτές τις δύο βιβλιοθήκες δεν υπάρχει κανένας ξεκάθαρος τρόπος για να χειριστείτε ασύγχρονες κλήσεις στο API (backend) ή άλλες παρενέργειες.

Σε αυτό το άρθρο προσπαθώ να συγκρίνω διαφορετικές προσεγγίσεις για την επίλυση αυτού του προβλήματος. Ας ορίσουμε πρώτα το πρόβλημα.

Το στοιχείο X είναι ένα από τα πολλά στοιχεία του ιστότοπου (ή για κινητά ή για επιτραπέζιους υπολογιστές, είναι επίσης δυνατό). X ερωτά και εμφανίζει ορισμένα δεδομένα που φορτώθηκαν από το API. Το X μπορεί να είναι σελίδα ή μόνο μέρος της σελίδας. Σημαντικό είναι ότι το X είναι ένα ξεχωριστό στοιχείο που πρέπει να συνδυάζεται χαλαρά με το υπόλοιπο σύστημα (όσο το δυνατόν περισσότερο). Το X πρέπει να εμφανίζει ένδειξη φόρτωσης κατά την ανάκτηση δεδομένων και το σφάλμα εάν η κλήση αποτύχει.

Αυτό το άρθρο προϋποθέτει ότι έχετε ήδη κάποια εμπειρία με τη δημιουργία εφαρμογών React / Redux.

Αυτό το άρθρο θα δείξει 4 τρόπους επίλυσης αυτού του προβλήματος και θα συγκρίνει τα πλεονεκτήματα και τα μειονεκτήματα του καθενός. Δεν είναι ένα λεπτομερές εγχειρίδιο για το πώς να χρησιμοποιήσετε thunk, έπος, αγωνίας ή άγκιστρα .

Κωδικός αυτών των παραδειγμάτων είναι διαθέσιμος στο GitHub.

Αρχική εγκατάσταση

Διακομιστής

Για σκοπούς δοκιμών θα χρησιμοποιήσουμε τον διακομιστή json. Είναι ένα καταπληκτικό έργο που σας επιτρέπει να δημιουργείτε ψεύτικα API REST πολύ γρήγορα. Για παράδειγμα, μοιάζει με αυτό.

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

Μετά την εκκίνηση του διακομιστή, μια κλήση στο // localhost: 4000 / users επιστρέφει τη λίστα των χρηστών με απομίμηση καθυστέρησης - περίπου 2 δευτερόλεπτα.

Κλήση έργου και API

Τώρα είμαστε έτοιμοι να ξεκινήσουμε την κωδικοποίηση. Υποθέτω ότι έχετε ήδη δημιουργήσει ένα έργο React χρησιμοποιώντας την εφαρμογή create-react-app με το Redux ρυθμισμένο και έτοιμο για χρήση.

Εάν έχετε δυσκολίες με αυτό μπορείτε να δείτε αυτό και αυτό.

Το επόμενο βήμα είναι να δημιουργήσετε μια συνάρτηση για να καλέσετε το 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 είναι ένα προτεινόμενο ενδιάμεσο λογισμικό για βασική λογική Redux παρενεργειών, όπως απλή λογική ασύγχρονης (όπως ένα αίτημα προς το API). Το ίδιο το Redux-thunk δεν κάνει πολλά. Είναι μόλις 14 !!! γραμμές του κώδικα. Απλώς προσθέτει λίγο «συντακτικό σάκχαρο» και τίποτα περισσότερο.

Το παρακάτω διάγραμμα ροής βοηθά να κατανοήσουμε τι πρόκειται να κάνουμε.

Κάθε φορά που εκτελείται μια ενέργεια, ο μειωτής αλλάζει την κατάσταση ανάλογα. Το στοιχείο χαρτών καταστάσεων σε ιδιότητες και χρησιμοποιεί αυτές τις ιδιότητες στη μέθοδο revder () για να καταλάβει τι πρέπει να δει ο χρήστης: ένδειξη φόρτωσης, δεδομένα ή μήνυμα σφάλματος.

Για να λειτουργήσει πρέπει να κάνουμε 5 πράγματα.

1. Εγκαταστήστε το δοχείο

npm install redux-thunk

2. Προσθέστε thunk middleware κατά τη διαμόρφωση του store (configigureStore.js)

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

Στις γραμμές 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() )};

Συνιστάται επίσης να διαχωρίσετε τους δημιουργούς δράσης σας (προσθέτει κάποια επιπλέον κωδικοποίηση), αλλά για αυτήν την απλή περίπτωση, νομίζω ότι είναι αποδεκτό να δημιουργείτε ενέργειες "εν κινήσει".

4. Δημιουργία 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. Δημιουργήστε ένα στοιχείο συνδεδεμένο στο 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);

Προσπάθησα να κάνω το στοιχείο όσο πιο απλό γίνεται. Καταλαβαίνω ότι φαίνεται απαίσιο :)

Φόρτωση δείκτη

Δεδομένα

Λάθος

Εκεί το έχετε: 3 αρχεία, 109 γραμμή κώδικα (13 (ενέργειες) + 36 (μειωτής) + 60 (στοιχείο)).

Πλεονεκτήματα:

  • Προτεινόμενη προσέγγιση για εφαρμογές αντιδράσεων / redux.
  • Χωρίς επιπλέον εξαρτήσεις. Σχεδόν, το thunk είναι μικρό :)
  • Δεν χρειάζεται να μάθετε νέα πράγματα.

Μειονεκτήματα:

  • Πολύς κώδικας σε διαφορετικά μέρη
  • 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!!!

Πλεονεκτήματα:

  • Δεν απαιτείται redux. Αυτή η προσέγγιση μπορεί να χρησιμοποιηθεί χωρίς redux. Το στοιχείο είναι πλήρως ανεξάρτητο.
  • Χωρίς επιπλέον εξαρτήσεις
  • Περίπου 2 φορές λιγότερος κωδικός από ό, τι σε άλλες λύσεις

Μειονεκτήματα:

  • Με την πρώτη ματιά, ο κώδικας φαίνεται περίεργος και δύσκολο να διαβαστεί και να κατανοηθεί. Θα χρειαστεί λίγος χρόνος για να συνηθίσετε τα άγκιστρα.
  • Ορισμένες νέες έννοιες πρέπει να μάθουν (που αποτελούν μέρος του React)
  • Δεν έχει κυκλοφορήσει επίσημα ακόμα

συμπέρασμα

Ας οργανώσουμε πρώτα αυτές τις μετρήσεις ως πίνακα.

  • Το Redux εξακολουθεί να είναι μια καλή επιλογή για τη διαχείριση του παγκόσμιου κράτους (εάν το έχετε)
  • Κάθε επιλογή έχει πλεονεκτήματα και μειονεκτήματα. Ποια προσέγγιση είναι καλύτερη εξαρτάται από το έργο: την πολυπλοκότητά του, τις περιπτώσεις χρήσης, την ομαδική γνώση, πότε το έργο πρόκειται να παραχθεί κ.λπ.
  • Το Saga μπορεί να βοηθήσει σε περίπλοκες περιπτώσεις χρήσης
  • Το Suspense και το Hooks αξίζουν και οι δύο (ή τουλάχιστον να μάθουν) ειδικά για νέα έργα

Αυτό είναι - απολαύστε και χαρούμενη κωδικοποίηση!