de Valerii Tereshchenko

Cum se încarcă datele în React cu redux-thunk, redux-saga, suspans și cârlige

Cum se incarca datele in React cu redux thunk redux saga suspans

Introducere

Reacţiona este o bibliotecă JavaScript pentru construirea interfețelor utilizator. Foarte des folosind React înseamnă să folosești React with Redux. Redux este o altă bibliotecă JavaScript pentru gestionarea stării globale. Din păcate, chiar și cu aceste două biblioteci nu există o modalitate clară de gestionare a apelurilor asincrone către API (backend) sau orice alte efecte secundare.

În acest articol încerc să compar diferite abordări pentru rezolvarea acestei probleme. Să definim mai întâi problema.

Componenta X este una dintre numeroasele componente ale site-ului web (sau aplicația mobilă sau desktop, este de asemenea posibilă). X interogă și arată câteva date încărcate din API. X poate fi o pagină sau doar o parte a paginii. Important este că X este o componentă separată care ar trebui să fie cuplată slab cu restul sistemului (pe cât posibil). X ar trebui să afișeze indicatorul de încărcare în timp ce datele sunt recuperate și erori dacă apelul eșuează.

Acest articol presupune că aveți deja o anumită experiență în crearea aplicațiilor React / Redux.

ad-banner

Acest articol va arăta 4 moduri de a rezolva această problemă și compara argumentele pro și contra din fiecare. Nu este un manual detaliat cu privire la modul de utilizare a thunk, saga, suspensie sau cârlige.

Codul acestor exemple este disponibil pe GitHub.

Configurare inițială

Mock Server

În scopuri de testare, vom folosi json-server. Este un proiect uimitor care vă permite să creați foarte repede API-uri REST false. Pentru exemplul nostru, arată așa.

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

Fișierul nostru db.json conține date de testare în format 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   } ]}

După pornirea serverului, un apel către http: // localhost: 4000 / utilizatori returnează lista utilizatorilor cu o imitație de întârziere – aproximativ 2s.

Apel de proiect și API

Acum suntem gata să începem codarea. Presupun că aveți deja un proiect React creat folosind creați-reacționați-aplicație cu Redux configurat și gata de utilizare.

Dacă aveți dificultăți cu aceasta, puteți verifica acest și acest.

Următorul pas este crearea unei funcții pentru apelarea API-ului (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 este un middleware recomandat pentru logica de bază a efectelor secundare Redux, cum ar fi logica asincronică simplă (cum ar fi o cerere către API). Redux-thunk în sine nu face prea multe. E doar 14 !!! linii de cod. Acesta adaugă doar niște „zahăr de sintaxă” și nimic mai mult.

Diagrama de mai jos vă ajută să înțelegeți ce vom face.

Cum se incarca datele in React cu redux thunk redux saga suspans

De fiecare dată când se efectuează o acțiune, reductorul schimbă starea în consecință. Componenta mapează starea la proprietăți și folosește aceste proprietăți în revder () metodă pentru a afla ce ar trebui să vadă utilizatorul: un indicator de încărcare, date sau mesaj de eroare.

Pentru ca acesta să funcționeze, trebuie să facem 5 lucruri.

1. Instalați tunk

npm install redux-thunk

2. Adăugați middleware thunk la configurarea magazinului (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;}

De asemenea, în liniile 12-13 configurăm redux devtools. Puțin mai târziu, vă va ajuta să arătați una dintre problemele cu această soluție.

3. Creați acțiuni (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!!!' })       )};

De asemenea, se recomandă separarea creatorilor de acțiuni (se adaugă câteva coduri suplimentare), dar pentru acest caz simplu cred că este acceptabil să creați acțiuni „din mers”.

4. Creați reductor (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. Creați o componentă conectată la 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 &lt;div>Loading</div>       }
       if (this.props.error) {           return &lt;div style={{ color: 'red' }}>ERROR: {this.props.error}</div>       }
       return (           &lt;table>               <thead>                   <tr>                       <th>First Name</th&gt;                       <th>Last Name</th&gt;                       &lt;th>;Active?</th>                       <th>Posts</th&gt;                       <th>Messages</th>                   &lt;/tr>               </thead>               <tbody>               {this.props.data.map(u =>                   <tr key={u.id}>                       &lt;td&gt;{u.firstName}</td>                       &lt;td>{u.lastName}</td&gt;                       <td>{u.active ? 'Yes' : 'No'}</td>                       <;td&gt;{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);

Am încercat să fac componenta cât mai simplă posibil. Înțeleg că arată îngrozitor 🙂

Indicator de încărcare

1612040888 153 Cum se incarca datele in React cu redux thunk redux saga suspans

Date

1612040888 656 Cum se incarca datele in React cu redux thunk redux saga suspans

Eroare

1612040888 448 Cum se incarca datele in React cu redux thunk redux saga suspans

Acolo îl aveți: 3 fișiere, 109 linii de cod (13 (acțiuni) + 36 (reductor) + 60 (componentă)).

Pro:

  • Abordare „recomandată” pentru aplicațiile react / redux.
  • Fără dependențe suplimentare. Aproape, thunk este mic 🙂
  • Nu este nevoie să înveți lucruri noi.

Contra:

  • O mulțime de cod în diferite locuri
  • După navigarea către o altă pagină, datele vechi sunt încă în starea globală (vezi imaginea de mai jos). Aceste date sunt informații învechite și inutile care consumă memorie.
  • În cazul unor scenarii complexe (apeluri condiționale multiple într-o acțiune etc.) codul nu este foarte ușor de citit
1612040888 457 Cum se incarca datele in React cu redux thunk redux saga suspans

Redux-saga

Redux-saga este o bibliotecă middleware redux concepută pentru a face ușor și ușor de citit gestionarea efectelor secundare. Utilizează generatorii ES6, ceea ce ne permite să scriem cod asincron care arată sincron. De asemenea, această soluție este ușor de testat.

Dintr-o perspectivă la nivel înalt, această soluție funcționează la fel ca Thunk. Diagrama de flux din exemplul thunk este încă aplicabilă.

Pentru ca acesta să funcționeze, trebuie să facem 6 lucruri.

1. Instalați saga

npm install redux-saga

2. Adăugați middleware saga și adăugați toate saga (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;}

Saga din linia 4 va fi adăugată la pasul 4.

3. Creați acțiune (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. Creați saga (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 are o curbă de învățare destul de abruptă, deci dacă nu ați folosit-o niciodată și nu ați citit niciodată nimic despre acest cadru, ar putea fi dificil să înțelegeți ce se întâmplă aici. Pe scurt, în userSaga funcție configurăm saga pentru a asculta LOAD_USERS_LOADING acțiune și declanșează fetchUsers funcţie. fetchUsers funcția apelează API-ul. Dacă apelul reușește, atunci LOAD_USER_SUCCESS acțiunea este expediată, în caz contrar LOAD_USER_ERROR acțiunea este expediată.

5. Creați reductor (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;       }   }}

Reductorul de aici este absolut același cu cel din exemplul Thunk.

6. Creați o componentă conectată la 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 &lt;div>Loading</div>       }
       if (this.props.error) {           return &lt;div style={{color: 'red'}}>ERROR: {this.props.error}</div>       }
       return (           &lt;table>               <thead>                   <tr>                       <th>First Name</th&gt;                       <th>Last Name</th&gt;                       &lt;th>;Active?</th>                       <th>Posts</th&gt;                       <th>Messages</th>                   </tr>;               </thead>               <tbody>                   {this.props.data.map(u =>                       <tr key={u.id}>                           &lt;td&gt;{u.firstName}&lt;/td>                           <td>;{u.lastName}</td>                           <td>{u.active ? 'Yes' : 'No'}</td>                           <td>{u.posts}&lt;/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);

Componenta este, de asemenea, aproape aceeași aici ca în exemplul thunk.

Deci, aici avem 4 fișiere, 136 linie de cod (7 (acțiuni) + 36 (reductor) + saga (33) + 60 (componentă)).

Pro:

  • Cod mai lizibil (asincronizat / await)
  • Bun pentru gestionarea scenariilor complexe (mai multe apeluri condiționate într-o acțiune, acțiunea poate avea mai mulți ascultători, acțiuni de anulare etc.)
  • Test de unitate ușor

Contra:

  • O mulțime de cod în diferite locuri
  • După navigarea către o altă pagină, datele vechi sunt încă în starea globală. Aceste date sunt informații învechite și inutile care consumă memorie.
  • Dependență suplimentară
  • O mulțime de concepte de învățat

Suspans

Suspansul este o caracteristică nouă în React 16.6.0. Ne permite să amânăm redarea unei părți a componentei până când sunt îndeplinite anumite condiții (de exemplu, datele din API-ul încărcat).

Pentru ca acesta să funcționeze, trebuie să facem 4 lucruri (cu siguranță se îmbunătățește :)).

1. Creați cache (suspans / cache.js)

Pentru cache, vom folosi un simplu-cache-provider care este un furnizor cache de bază pentru aplicațiile react.

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

2. Creați limite de eroare (suspans / ErrorBoundary.js)

Aceasta este o limită de eroare pentru a prinde erorile aruncate de 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 &lt;div style={{ color: 'red' }}>ERROR: {this.state.error || 'Unexpected Error'}</div>;   }
   return this.props.children; }}
export default ErrorBoundary;

3. Creați tabelul utilizatorilor (suspans / UsersTable.js)

Pentru acest exemplu, trebuie să creăm o componentă suplimentară care încarcă și afișează date. Aici creăm o resursă pentru a obține date din 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 (           &lt;table>               <thead>               <tr>                   <th>First Name&lt;/th>                   <th>;Last Name&lt;/th>                   <th&gt;Active?</th>                   <th>Posts&lt;/th>                   <th>Messages</th>               </tr>               </thead&gt;               <tbody&gt;               {users.map(u =>                   <tr key={u.id}>                       &lt;td&gt;{u.firstName}</td>                       &lt;td>{u.lastName}</td&gt;                       <td>{u.active ? 'Yes' : 'No'}</td>                       <;td&gt;{u.posts}</td>                       <td>{u.messages}</td>                   </tr>               )}               </tbody>           </table>       );   }}
export default UsersTable;

4. Creați o componentă (suspans / UsersWithSuspense.js)

import * as React from 'react';import UsersTable from "./UsersTable";import ErrorBoundary from "./ErrorBoundary";
class UsersWithSuspense extends React.Component {   render() {       return (           &lt;ErrorBoundary>               <React.Suspense fallback={<div&gt;Loading</div>}&gt;                   <UsersTable/>               &lt;/React.Suspense>           </ErrorBoundary>       );   }}
export default UsersWithSuspense;

4 fișiere, 106 linie de cod (9 (cache) + 19 (ErrorBoundary) + UsersTable (33) + 45 (component)).

3 fișiere, 87 linie de cod (9 (cache) + UsersTable (33) + 45 (component)) dacă presupunem că ErrorBoundary este o componentă reutilizabilă.

Pro:

  • Nu este nevoie de redux. Această abordare poate fi utilizată fără redux. Componenta este complet independentă.
  • Fără dependențe suplimentare (simplu-cache-provider face parte din React)
  • Întârzierea afișării indicatorului de încărcare prin setarea proprietății dellayMs
  • Mai puține linii de cod decât în ​​exemplele anterioare

Contra:

  • Memoria cache este necesară chiar și atunci când nu avem nevoie de cache.
  • Unele concepte noi trebuie învățate (care fac parte din React).

Cârlige

La momentul redactării acestui articol, cârligele nu au fost încă lansate oficial și disponibile doar în versiunea „următoare”. Cârligele sunt incontestabil una dintre cele mai revoluționare caracteristici viitoare care se pot schimba foarte mult în lumea React foarte curând. Mai multe detalii despre cârlige puteți găsi Aici și Aici.

Pentru ca acesta să funcționeze pentru exemplul nostru, trebuie să facem acest lucru unu(!) lucru:

1. Creați și utilizați cârlige (hooks / UsersWithHooks.js)

Aici creăm 3 cârlige (funcții) pentru a „agăța” starea 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 &lt;div>Loading</div>   }
   if (error) {       return &lt;div style={{color: 'red'}}>ERROR: {error}</div>   }
   return (       &lt;table>           <thead>           <tr>;               <th>First Name</th>               <th&gt;Last Name</th>               &lt;th>Active?</th>               <th&gt;Posts</th&gt;               &lt;th>Messages</th>           </tr&gt;           </thead>           <tbody>           {data.map(u =&gt;               <tr key={u.id}>;                   &lt;td>;{u.firstName}</td>;                   <td>{u.lastName}&lt;/td&gt;                   &lt;td>{u.active ? 'Yes' : 'No'}<;/td>                   <td>{u.posts}</td>                   <td>{u.messages}</td>               </tr>           )}           </tbody>       </table>   );}
export default UsersWithHooks;

Și atât – doar 1 fișier, 56 de linii de cod !!!

Pro:

  • Nu este nevoie de redux. Această abordare poate fi utilizată fără redux. Componenta este complet independentă.
  • Fără dependențe suplimentare
  • Cod de aproximativ 2 ori mai puțin decât în ​​alte soluții

Contra:

  • La prima vedere, codul pare ciudat și greu de citit și de înțeles. Va dura ceva timp să te obișnuiești cu cârligele.
  • Unele concepte noi trebuie învățate (care fac parte din React)
  • Nu a fost lansat oficial încă

Concluzie

Să organizăm mai întâi aceste valori ca tabel.

1612040889 406 Cum se incarca datele in React cu redux thunk redux saga suspans
  • Redux este încă o opțiune bună pentru a gestiona starea globală (dacă o aveți)
  • Fiecare opțiune are argumente pro și contra. Ce abordare este mai bună depinde de proiect: complexitatea acestuia, cazurile de utilizare, cunoștințele echipei, când proiectul merge la producție etc.
  • Saga poate ajuta cu cazuri de utilizare complexe
  • Suspansul și Hooks sunt ambele demne de luat în considerare (sau cel puțin de învățat) în special pentru proiecte noi

Gata – bucurați-vă și codificați fericit!