كيفية تحميل البيانات في React باستخدام redux-thunk و redux-saga والتشويق والخطافات

المقدمة

React هي مكتبة JavaScript لبناء واجهات المستخدم. غالبًا ما يعني استخدام React استخدام React مع Redux. Redux هي مكتبة JavaScript أخرى لإدارة الحالة العالمية. للأسف ، حتى مع هاتين المكتبتين ، لا توجد طريقة واحدة واضحة للتعامل مع الاستدعاءات غير المتزامنة لواجهة برمجة التطبيقات (الواجهة الخلفية) أو أي آثار جانبية أخرى.

في هذه المقالة أحاول مقارنة الأساليب المختلفة لحل هذه المشكلة. دعنا نحدد المشكلة أولا.

يعد المكون X أحد المكونات العديدة لموقع الويب (أو تطبيق الجوال أو سطح المكتب ، وهو ممكن أيضًا). يستعلم X ويظهر بعض البيانات التي تم تحميلها من API. يمكن أن يكون X صفحة أو مجرد جزء من الصفحة. الشيء المهم هو أن X هو مكون منفصل يجب أن يقترن بشكل فضفاض ببقية النظام (قدر الإمكان). يجب أن يُظهر X مؤشر التحميل أثناء استرداد البيانات والخطأ في حالة فشل المكالمة.

تفترض هذه المقالة أن لديك بالفعل بعض الخبرة في إنشاء تطبيقات React / Redux.

ستعرض هذه المقالة 4 طرق لحل هذه المشكلة ومقارنة إيجابيات وسلبيات كل منها. إنه ليس دليلاً مفصلاً حول كيفية استخدام thunk أو saga أو معلق أو خطافات .

رمز هذه الأمثلة متاح على جيثب.

الإعداد الأولي

خادم وهمي

لأغراض الاختبار ، سنستخدم خادم json. إنه مشروع مذهل يتيح لك إنشاء واجهات برمجة تطبيقات 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 الأساسي ، مثل منطق غير متزامن بسيط (مثل طلب واجهة برمجة التطبيقات). Redux-thunk نفسها لا تفعل الكثير. إنها 14 سنة فقط !!! أسطر الكود. إنه يضيف فقط بعض "السكر النحوي" ولا شيء أكثر.

يساعد المخطط الانسيابي أدناه على فهم ما سنفعله.

في كل مرة يتم تنفيذ إجراء ما ، يتغير المخفض وفقًا لذلك. يشير المكون إلى الخصائص ويستخدم هذه الخصائص في طريقة revder () لمعرفة ما يجب أن يراه المستخدم: مؤشر تحميل أو بيانات أو رسالة خطأ.

لجعلها تعمل ، نحتاج إلى القيام بخمسة أشياء.

1. تثبيت tunk

npm install redux-thunk

2. إضافة برنامج وسيط thunk عند تكوين store (configStore.js)

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

في السطور 12-13 ، نقوم أيضًا بتهيئة أدوات تطوير إعادة التشغيل. بعد ذلك بقليل ، سيساعد ذلك في إظهار إحدى المشكلات المتعلقة بهذا الحل.

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. إنشاء المخفض (redux-thunk / المخفض.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-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 (مكون)).

الايجابيات:

  • نهج "موصى به" لتطبيقات التفاعل / الإعادة.
  • لا تبعيات إضافية. تقريبا ، 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!!!

الايجابيات:

  • لا حاجة لإعادة. يمكن استخدام هذا النهج دون إعادة. المكون مستقل تماما.
  • لا تبعيات إضافية
  • حوالي 2 مرات رمز أقل من الحلول الأخرى

سلبيات:

  • للوهلة الأولى ، يبدو الرمز غريبًا ويصعب قراءته وفهمه. سوف يستغرق الأمر بعض الوقت لتعتاد على الخطافات.
  • يجب تعلم بعض المفاهيم الجديدة (وهي جزء من React)
  • لم يصدر رسميا بعد

استنتاج

دعنا ننظم هذه المقاييس كجدول أولاً.

  • لا يزال Redux خيارًا جيدًا لإدارة الحالة العالمية (إذا كان لديك)
  • كل خيار له إيجابيات وسلبيات. يعتمد النهج الأفضل على المشروع: مدى تعقيده ، وحالات الاستخدام ، ومعرفة الفريق ، ومتى ينتقل المشروع إلى الإنتاج ، وما إلى ذلك.
  • يمكن أن تساعد Saga في حالات الاستخدام المعقدة
  • التشويق والخطافات كلاهما يستحق التفكير (أو على الأقل التعلم) خاصة بالنسبة للمشاريع الجديدة

هذا كل شيء - استمتع بالبرمجة السعيدة!