web-dev-qa-db-fra.com

Avantages / inconvénients de l'utilisation de redux-saga avec les générateurs ES6 par rapport à redux-thunk avec ES2017 async / wait

On parle beaucoup du dernier gosse de la ville de Redux en ce moment, redux-saga/redux-saga . Il utilise les fonctions du générateur pour écouter/répartir les actions.

Avant de commencer, j'aimerais connaître le pour et le contre de l'utilisation de redux-saga au lieu de l'approche ci-dessous où j'utilise redux-thunk avec async/wait.

Un composant peut ressembler à ceci, les actions de dispatch comme d'habitude.

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

Ensuite, mes actions ressemblent à ceci:

// auth.js

import request from 'axios';
import { loadUserData } from './user';

// define constants
// define initial state
// export default reducer

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

// more actions...

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

// more actions...
437
hampusohlsson

Dans redux-saga, l'équivalent de l'exemple ci-dessus serait

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST)
    try {
      let { data } = yield call(request.post, '/login', { user, pass });
      yield fork(loadUserData, data.uid);
      yield put({ type: LOGIN_SUCCESS, data });
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

La première chose à noter est que nous appelons les fonctions api sous la forme yield call(func, ...args). call n'exécute pas l'effet, il crée simplement un objet simple comme {type: 'CALL', func, args}. L'exécution est déléguée au middleware redux-saga qui se charge de l'exécution de la fonction et de la reprise du générateur avec son résultat.

Le principal avantage est que vous pouvez tester le générateur en dehors de Redux en utilisant de simples contrôles d'égalité.

const iterator = loginSaga()

assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

Notez que nous nous moquons du résultat de l'appel api en injectant simplement les données simulées dans la méthode next de l'itérateur. La simulation de données est beaucoup plus simple que les fonctions de simulation.

La deuxième chose à noter est l'appel à yield take(ACTION). Les créateurs d’action appellent des thunks à chaque nouvelle action (par exemple LOGIN_REQUEST). c'est-à-dire que les actions sont continuellement poussées sur thunks, et que thunks n'a aucun contrôle sur le moment opportun pour arrêter de gérer ces actions.

Dans redux-saga, les générateurs tirent l'action suivante. c'est-à-dire qu'ils ont le contrôle quand il faut écouter pour une action et quand ne pas le faire. Dans l'exemple ci-dessus, les instructions de flux sont placées à l'intérieur d'une boucle while(true), de manière à ce que chaque action entrante soit écoutée, ce qui imite un peu le comportement d'appétit.

L'approche par traction permet de mettre en œuvre des flux de contrôle complexes. Supposons par exemple que nous voulions ajouter les exigences suivantes

  • Gestion de l'action utilisateur LOGOUT

  • lors de la première connexion réussie, le serveur renvoie un jeton qui expire dans un certain délai, stocké dans un champ expires_in. Nous devrons actualiser l'autorisation en arrière-plan à chaque expires_in millisecondes

  • Tenez compte du fait que lorsqu’il attend le résultat des appels de l’API (connexion initiale ou actualisation), l’utilisateur peut se déconnecter entre deux sessions.

Comment mettriez-vous cela en œuvre avec des thunks? tout en fournissant également une couverture de test complète pour l'ensemble du flux? Voici à quoi cela peut ressembler avec les Sagas:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}

function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires_in)
    token = yield call(authorize, {token})
  }
}

function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN_REQUEST)

      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

Dans l'exemple ci-dessus, nous exprimons notre exigence de concurrence en utilisant race. Si take(LOGOUT) gagne la course (l’utilisateur a cliqué sur un bouton de déconnexion). La course annulera automatiquement la tâche en arrière-plan authAndRefreshTokenOnExpiry. Et si la authAndRefreshTokenOnExpiry était bloquée au milieu d'un appel call(authorize, {token}), elle serait également annulée. L'annulation se propage automatiquement vers le bas.

Vous pouvez trouver un démo exécutable du flux ci-dessus

430
Yassine Elouafi

J'ajouterai mon expérience d'utilisation de saga dans le système de production à la réponse plutôt détaillée de l'auteur de la bibliothèque.

Pro (en utilisant la saga):

  • Testabilité. Il est très facile de tester les sagas car call () renvoie un objet pur. Pour tester thunks, vous devez normalement inclure un mockStore dans votre test.

  • redux-saga contient de nombreuses fonctions utiles sur les tâches. Il me semble que le concept de saga est de créer un type de travailleur/fil d’arrière-plan pour votre application, qui fait office de pièce manquante dans l’architecture redux de réaction (actionCreators et réducteurs doivent être des fonctions pures.) Ce qui conduit au point suivant.

  • Les sagas offrent un endroit indépendant pour gérer tous les effets secondaires. Il est généralement plus facile de modifier et de gérer que les actions simples dans mon expérience.

Con:

  • Syntaxe du générateur.

  • Beaucoup de concepts à apprendre.

  • Stabilité de l'API. Il semble que redux-saga ajoute encore des fonctionnalités (par exemple, des chaînes?) Et la communauté n’est pas aussi importante. Il y a un problème si la bibliothèque effectue un jour une mise à jour non compatible avec les versions antérieures.

88
yjcxy12

J'aimerais juste ajouter quelques commentaires de mon expérience personnelle (en utilisant à la fois des sagas et du thunk):

Les Sagas sont super à tester:

  • Vous n'avez pas besoin de vous moquer de fonctions enveloppées d'effets
  • Par conséquent, les tests sont propres, lisibles et faciles à écrire
  • Lors de l'utilisation de sagas, les créateurs d'action renvoient généralement des littéraux d'objet standard. Il est également plus facile de tester et d’affirmer contrairement aux promesses de thunk.

Les Sagas sont plus puissants. Tout ce que vous pouvez faire dans le créateur d'action d'un thunk peut également l'être dans une saga, mais pas l'inverse (ou du moins, pas facilement). Par exemple:

  • attendre qu'une action/des actions soient envoyées (take)
  • annuler la routine existante (cancel, takeLatest, race)
  • plusieurs routines peuvent écouter la même action (take, takeEvery, ...)

Sagas propose également d’autres fonctionnalités utiles, généralisant certains modèles d’application courants:

  • channels pour écouter des sources d'événements externes (par exemple, des websockets)
  • modèle de fourche (fork, spawn)
  • throttle
  • ...

Les Sagas sont un outil formidable et puissant. Cependant, avec le pouvoir vient la responsabilité. Lorsque votre demande augmente, vous pouvez facilement vous perdre en déterminant qui attend l'envoi de l'action ou tout ce qui se passe lors de l'envoi d'une action. D'autre part, thunk est plus simple et plus facile à raisonner. Le choix de l'un ou de l'autre dépend de nombreux aspects, tels que le type et la taille du projet, les types d'effets secondaires que votre projet doit gérer ou les préférences de l'équipe de développement. Dans tous les cas, il suffit de garder votre application simple et prévisible.

25
madox2

Juste une expérience personnelle:

  1. Pour ce qui est du style de codage et de la lisibilité, l’un des avantages les plus importants de l’utilisation de redux-saga dans le passé est d’éviter tout rappel par call-back dans redux-thunk - il n’est plus nécessaire d’utiliser beaucoup d’emboîtements à ce moment-là. Mais maintenant, avec la popularité de async/wait thunk, on peut aussi écrire du code asynchrone dans un style synchronisé avec redux-thunk, ce qui peut être considéré comme une amélioration de redux-think.

  2. Vous devrez peut-être écrire beaucoup plus de code standard lorsque vous utiliserez redux-saga, en particulier dans TypeScript. Par exemple, si l'on souhaite implémenter une fonction d'extraction asynchrone, la gestion des données et des erreurs peut être effectuée directement dans une unité thunk dans action.js avec une seule action FETCH. Mais dans redux-saga, il peut être nécessaire de définir les actions FETCH_START, FETCH_SUCCESS et FETCH_FAILURE et toutes les vérifications de type correspondantes, car l’une des fonctionnalités de redux-saga est d’utiliser ce type de mécanisme riche de "jetons" pour créer des effets et Redux Store pour des tests faciles. Bien sûr, on pourrait écrire une saga sans utiliser ces actions, mais cela ressemblerait à un thunk.

  3. En termes de structure de fichier, redux-saga semble être plus explicite dans de nombreux cas. On pourrait facilement trouver un code lié asynchrone dans chaque fichier sagas.ts, mais dans redux-thunk, il faudrait le voir dans les actions.

  4. Des tests faciles peuvent constituer une autre caractéristique pondérée de Redux-Saga. C'est vraiment pratique. Une chose à clarifier toutefois est que le test “d'appel” de redux-saga n'effectue pas un appel API réel lors du test. Par conséquent, il est nécessaire de spécifier le résultat de l'échantillon pour les étapes pouvant l'utiliser après l'appel API. Par conséquent, avant d'écrire dans redux-saga, il serait préférable de planifier une saga et ses sagas.spec.ts correspondantes en détail.

  5. Redux-saga fournit également de nombreuses fonctionnalités avancées telles que l'exécution de tâches en parallèle, des aides à la concurrence telles que takeLatest/takeEvery, fork/spawn, qui sont bien plus puissants que Thunks.

En conclusion, personnellement, je voudrais dire ceci: dans de nombreux cas normaux et dans des applications de taille petite à moyenne, optez pour le style async/wait style redux-thunk. Cela vous épargnerait de nombreux codes/actions/typedefs standard, et vous n'auriez pas besoin de changer de sagas.ts et de maintenir un arbre de sagas spécifique. Mais si vous développez une application volumineuse avec une logique asynchrone beaucoup complexe et que vous avez besoin de fonctionnalités telles que la configuration simultanée/parallèle, ou si vous avez une forte demande en tests et en maintenance (en particulier dans le développement piloté par les tests), redux-sagas vous sauverait probablement la vie. .

Quoi qu'il en soit, redux-saga n'est pas plus difficile et complexe que redux lui-même, et ne présente pas de courbe d'apprentissage dite abrupte, car ses concepts de base et API sont bien limités. Consacrer un peu de temps à l’apprentissage de redux-saga peut vous aider un jour dans le futur.

1
Jonathan

Un moyen plus simple consiste à utiliser redux-auto .

de la documantasion

redux-auto a résolu ce problème asynchrone simplement en vous permettant de créer une fonction "action" renvoyant une promesse. Pour accompagner la logique d'action de votre fonction "par défaut".

  1. Pas besoin d'autres middlewares asynchrones Redux. par exemple. thunk, middleware de promesse, saga
  2. Permet facilement de passer une promesse dans redux et de la gérer pour vous
  3. Vous permet de co-localiser des appels de service externes avec l'endroit où ils seront transformés
  4. Nommer le fichier "init.js" l'appellera une fois au démarrage de l'application. C'est bon pour charger les données du serveur au démarrage

L'idée est d'avoir chaque action dans un fichier spécifique . co-localiser l'appel du serveur dans le fichier avec les fonctions de réduction pour "en attente", "rempli" et "rejeté". Cela rend les promesses de manipulation très faciles.

Il attache également automatiquement un objet d'assistance (appelé "asynchrone") au prototype de votre état, ce qui vous permet de suivre dans votre interface utilisateur les transitions demandées.

0
codemeasandwich

Thunks versus Sagas

Redux-Thunk et Redux-Saga diffèrent de manière importante, les deux étant des bibliothèques de middleware pour Redux (le middleware Redux est un code qui intercepte les actions entrant dans le magasin via la méthode dispatch ()).

Une action peut être littéralement n'importe quoi, mais si vous suivez les meilleures pratiques, elle est un objet JavaScript simple avec un champ de type et des champs facultatifs de charge utile, de méta et d'erreur. par exemple.

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

En plus de l'envoi d'actions standard, le middleware Redux-Thunk vous permet d'envoyer des fonctions spéciales, appelées thunks.

Thunks (en Redux) ont généralement la structure suivante:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

C'est-à-dire que thunk est une fonction qui prend (éventuellement) certains paramètres et retourne une autre fonction. La fonction interne prend les fonctions dispatch function et getState, qui seront toutes deux fournies par le middleware Redux-Thunk.

Redux-Saga

Redux-Saga le middleware vous permet d'exprimer une logique d'application complexe sous forme de fonctions pures, appelées sagas. Les fonctions pures sont souhaitables du point de vue des tests car elles sont prévisibles et répétables, ce qui les rend relativement faciles à tester.

Les Sagas sont implémentés via des fonctions spéciales appelées fonctions génératrices. Ce sont une nouvelle fonctionnalité de ES6 JavaScript. En gros, l’exécution saute d’un générateur à l’autre où vous voyez une déclaration de rendement. Pensez à une instruction yield comme provoquant une pause du générateur et le renvoi de la valeur renvoyée. Plus tard, l'appelant peut reprendre le générateur à l'instruction suivant le yield.

Une fonction génératrice est définie comme celle-ci. Notez l'astérisque après le mot clé function.

function* mySaga() {
    // ...
}

Une fois que la saga de connexion est enregistrée avec Redux-Saga. Mais ensuite, la yield prise sur la première ligne mettra la saga en pause jusqu'à ce qu'une action de type 'LOGIN_REQUEST' soit envoyée au magasin. Une fois que cela se produit, l'exécution continuera.

Pour plus de détails, voir cet article .

0
Mselmi Ali

Un petit mot. Les générateurs sont annulables, asynchrones/wait - pas. Ainsi, pour un exemple tiré de la question, cela n’a pas vraiment de sens. Mais pour des flux plus compliqués, il n’ya parfois pas de meilleure solution que d’utiliser des générateurs.

Donc, une autre idée pourrait être d’utiliser des générateurs avec redux-thunk, mais pour moi, c’est comme essayer d’inventer un vélo à roues carrées.

Et bien sûr, les générateurs sont plus faciles à tester.

0
Dmitriy

Voici un projet qui combine les meilleures parties (pros) de redux-saga et redux-thunk: vous pouvez gérer tous les effets secondaires des sagas tout en obtenant une promesse de dispatching l'action correspondante: - https://github.com/diegohaz/redux-saga-thunk

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}
0
Diego Haz

Après avoir passé en revue quelques projets React/Redux de grande envergure selon mon expérience, Sagas offre aux développeurs une manière plus structurée d’écrire du code, beaucoup plus facile à tester et plus difficile à obtenir.

Oui, c'est un peu bizarre au début, mais la plupart des développeurs en comprennent assez en une journée. Je dis toujours aux gens de ne pas s'inquiéter de ce que yield fait au début et qu'une fois que vous aurez écrit un test, il vous sera envoyé.

J'ai vu quelques projets où des thunks ont été traités comme s'ils étaient des contrôleurs de MVC Patten, ce qui est rapidement devenu un gâchis incompréhensible.

Mon conseil est d'utiliser des sagas où vous avez besoin de déclencheurs de type B relatifs à un seul événement. Pour tout ce qui pourrait concerner plusieurs actions, j’ai trouvé qu’il était plus simple d’écrire un middleware client et d’utiliser la propriété méta d’une action de la FSA pour la déclencher.

0
David Bradshaw