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


488

Il y a beaucoup de discussions sur le dernier enfant de la ville de redux en ce moment, redux-saga / redux-saga . Il utilise des fonctions de générateur pour écouter / répartir les actions.

Avant d'envelopper ma tête, j'aimerais connaître les avantages / inconvénients de l'utilisation redux-sagaau lieu de l'approche ci-dessous où j'utilise redux-thunkavec async / wait.

Un composant peut ressembler à ceci, répartir les actions 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...

6
Voir aussi ma réponse comparant redux-thunk à redux-saga ici: stackoverflow.com/a/34623840/82609
Sebastien Lorber

22
Quel est le ::avant de this.onClickfaire?
Downhillski

37
@ZhenyangHua c'est un raccourci pour lier la fonction à l'objet ( this), alias this.onClick = this.onClick.bind(this). La forme plus longue est généralement recommandée dans le constructeur, car le raccourci se lie à nouveau à chaque rendu.
hampusohlsson

7
Je vois. Merci! Je vois des gens utiliser bind()beaucoup pour passer thisà la fonction, mais j'ai commencé à utiliser () => method()maintenant.
Downhillski

2
@Hosar J'ai utilisé redux et redux-saga en production pendant un certain temps, mais j'ai en fait migré vers MobX après quelques mois parce que les frais généraux sont moins
élevés

Réponses:


461

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 en utilisant le formulaire yield call(func, ...args). calln'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 d'exécuter la fonction et de reprendre le générateur avec son résultat.

Le principal avantage est que vous pouvez tester le générateur en dehors de Redux à l'aide de simples vérifications 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 nextméthode de l'itérateur. Les données de simulation sont bien plus simples que les fonctions de simulation.

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

Dans redux-saga, les générateurs tirent la prochaine action. c'est-à-dire qu'ils ont le contrôle quand écouter une action, et quand ne pas. Dans l'exemple ci-dessus, les instructions de flux sont placées dans une while(true)boucle, donc il écoutera chaque action entrante, ce qui imite quelque peu le comportement de poussée du thunk.

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

  • Gérer 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 expires_inchamp. Nous devrons actualiser l'autorisation en arrière-plan à chaque expires_inmilliseconde

  • Tenez compte du fait qu'en attendant le résultat des appels api (soit la connexion initiale, soit l'actualisation), l'utilisateur peut se déconnecter entre les deux.

Comment implémenteriez-vous cela avec des thunks; tout en offrant 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 à l'aide de race. Si take(LOGOUT)gagne la course (c'est-à-dire que l'utilisateur a cliqué sur un bouton de déconnexion). La course annulera automatiquement la authAndRefreshTokenOnExpirytâche d'arrière - plan. Et si le a authAndRefreshTokenOnExpiryété bloqué au milieu d'un call(authorize, {token})appel, il sera également annulé. L'annulation se propage automatiquement vers le bas.

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


@yassine d'où vient la delayfonction? Ah, je l' ai
philk

122
Le redux-thunkcode est assez lisible et s'explique de lui-même. Mais redux-sagason est vraiment illisible, principalement à cause de ceux-verbe comme des fonctions: call, fork, take, put...
syg

11
@syg, je suis d'accord que call, fork, take et put peuvent être plus conviviaux sur le plan sémantique. Cependant, ce sont ces fonctions semblables à des verbes qui rendent tous les effets secondaires testables.
Downhillski

3
@syg toujours une fonction avec ces fonctions de verbes étranges sont plus lisibles qu'une fonction avec une chaîne de promesses profondes
Yasser Sinjab

3
ces verbes "bizarres" vous aident également à conceptualiser la relation de la saga avec les messages provenant de redux. vous pouvez retirer les types de message de redux - souvent pour déclencher la prochaine itération, et vous pouvez remettre de nouveaux messages pour diffuser le résultat de votre effet secondaire.
worc

104

J'ajouterai mon expérience de l'utilisation de la saga dans le système de production en plus de la réponse plutôt approfondie de l'auteur de la bibliothèque.

Pro (en utilisant la saga):

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

  • redux-saga est livré avec de nombreuses fonctions d'aide utiles sur les tâches. Il me semble que le concept de saga est de créer une sorte de travailleur / thread d'arrière-plan pour votre application, qui agit comme une pièce manquante dans l'architecture React Redux (les créateurs d'action et les réducteurs doivent être des fonctions pures.) Ce qui nous amène au point suivant.

  • Les sagas offrent un lieu 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 de thunk selon 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 grande. Il y a un problème si la bibliothèque effectue un jour une mise à jour non rétrocompatible.


9
Je veux juste faire un commentaire, le créateur d'action n'a pas besoin d'être une fonction pure, ce qui a été affirmé par Dan lui-même à plusieurs reprises.
Marson Mao

14
À partir de maintenant, les redux-sagas sont très recommandés car l'utilisation et la communauté se sont développées. De plus, l'API est devenue plus mature. Envisagez de supprimer le Con pour API stabilityune mise à jour pour refléter la situation actuelle.
Denialos

1
la saga a plus de départs que le thunk et son dernier commit est aussi après le
thunk

2
Oui, FWIW redux-saga a maintenant 12k stars, redux-thunk a 8k
Brian Burns

3
Je vais ajouter un autre défi des sagas, c'est que les sagas sont entièrement dissociées des actions et des créateurs d'actions par défaut. Alors que Thunks connecte directement les créateurs d'action avec leurs effets secondaires, les sagas laissent les créateurs d'action totalement séparés des sagas qui les écoutent. Cela présente des avantages techniques, mais peut rendre le code beaucoup plus difficile à suivre et peut brouiller certains des concepts unidirectionnels.
theaceofthespade

33

Je voudrais 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 des 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'actions retournent généralement des littéraux d'objets simples. Il est également plus facile de tester et d'affirmer contrairement aux promesses de Thunk.

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

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

Sagas propose également d'autres fonctionnalités utiles, qui généralisent certains modèles d'application courants:

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

Les sagas sont un outil formidable et puissant. Mais avec le pouvoir vient la responsabilité. Lorsque votre application se développe, vous pouvez facilement vous perdre en déterminant qui attend que l'action soit distribuée ou ce qui se passe quand une action est distribuée. D'un autre côté, le thunk est plus simple et plus facile à raisonner. Le choix de l'un ou 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, gardez simplement votre application simple et prévisible.


8

Juste une expérience personnelle:

  1. Pour le style de codage et la lisibilité, l'un des avantages les plus importants de l'utilisation de redux-saga dans le passé est d'éviter l'enfer de rappel dans redux-thunk - on n'a plus besoin d'utiliser beaucoup d'imbrication then / catch. Mais maintenant, avec la popularité de l'async / wait thunk, on pourrait également écrire du code async dans le style de synchronisation lors de l'utilisation de redux-thunk, ce qui peut être considéré comme une amélioration de redux-think.

  2. Il peut être nécessaire d'écrire beaucoup plus de code passe-partout lors de l'utilisation de redux-saga, en particulier dans Typescript. Par exemple, si l'on veut implémenter une fonction d'extraction asynchrone, la gestion des données et des erreurs pourrait ê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 leurs vérifications de type associées, car l'une des fonctionnalités de redux-saga est d'utiliser ce type de mécanisme riche en «jetons» pour créer des effets et instruire magasin redux pour des tests faciles. Bien sûr, on pourrait écrire une saga sans utiliser ces actions, mais cela la rendrait semblable à un thunk.

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

  4. Les tests faciles peuvent être une autre fonctionnalité pondérée dans redux-saga. C'est vraiment pratique. Mais une chose qui doit être clarifiée est que le test "d'appel" de redux-saga n'effectuerait pas d'appel API réel lors des tests, donc il faudrait spécifier l'exemple de résultat pour les étapes qui peuvent 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 assistants de concurrence comme takeLatest / takeEvery, fork / spawn, qui sont beaucoup plus puissants que les thunks.

En conclusion, personnellement, je voudrais dire: dans de nombreux cas normaux et des applications de petite à moyenne taille, optez pour le style asynchrone / wait redux-thunk. Cela vous permettrait d'économiser de nombreux codes / actions / typedefs standard, et vous n'auriez pas besoin de basculer entre de nombreux sagas.ts différents et de maintenir un arbre de sagas spécifique. Mais si vous développez une grande application avec une logique asynchrone très complexe et le besoin de fonctionnalités telles que le modèle simultané / parallèle, ou si vous avez une forte demande de tests et de maintenance (en particulier dans le développement piloté par les tests), redux-sagas pourrait peut-être vous sauver la vie .

Quoi qu'il en soit, redux-saga n'est pas plus difficile et complexe que redux lui-même, et il n'a pas de courbe d'apprentissage dite abrupte car il a des concepts de base et des API bien limités. Passer un peu de temps à apprendre la redux-saga pourrait vous être utile un jour à l'avenir.


5

Après avoir passé en revue plusieurs projets React / Redux à grande échelle dans mon expérience, Sagas offre aux développeurs une manière plus structurée d'écrire du code qui est beaucoup plus facile à tester et plus difficile à se tromper.

Oui, c'est un peu bizarre pour commencer, mais la plupart des développeurs en comprennent assez en une journée. Je dis toujours aux gens de ne pas se soucier de ce yieldqui commence et qu'une fois que vous aurez passé quelques tests, cela vous reviendra.

J'ai vu quelques projets où les thunks ont été traités comme s'ils étaient des contrôleurs du patten MVC et cela devient rapidement un gâchis incontrôlable.

Mon conseil est d'utiliser des Sagas où vous avez besoin de déclencheurs A de type B relatifs à un seul événement. Pour tout ce qui pourrait recouper un certain nombre d'actions, je trouve qu'il est plus simple d'écrire le middleware client et d'utiliser la méta-propriété d'une action FSA pour la déclencher.


2

Thunks contre Sagas

Redux-Thunket Redux-Sagadiffèrent de plusieurs manières importantes, les deux sont 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 n'importe quoi, mais si vous suivez les meilleures pratiques, une action est un simple objet javascript 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 distribuer des actions standard, le Redux-Thunkmiddleware vous permet de distribuer des fonctions spéciales, appelées thunks.

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

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

Autrement dit, a thunkest une fonction qui (facultativement) prend certains paramètres et renvoie une autre fonction. La fonction interne prend une fonction dispatch functionet une getStatefonction, toutes deux fournies par le Redux-Thunkmiddleware.

Redux-Saga

Redux-Sagale 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ées via des fonctions spéciales appelées fonctions génératrices. Ce sont une nouvelle fonctionnalité de ES6 JavaScript. Fondamentalement, l'exécution saute dans et hors d'un générateur partout où vous voyez une déclaration de rendement. Considérez une yielddéclaration comme provoquant une pause du générateur et renvoyant la valeur produite. Plus tard, l'appelant peut reprendre le générateur à l'instruction suivant le yield.

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

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

Une fois la saga de connexion enregistrée Redux-Saga. Mais ensuite, la yieldprise 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 se poursuit.

Pour plus de détails, consultez cet article .


1

Une petite note. Les générateurs sont annulables, asynchrones / attendent - pas. Donc, pour un exemple de la question, cela n'a pas vraiment de sens de quoi choisir. Mais pour des flux plus compliqués, il n'y a 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 à redux-thunk, mais pour moi, cela ressemble à essayer d'inventer un vélo à roues carrées.

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


0

Voici un projet qui combine les meilleures parties (pros) des deux redux-sagaet redux-thunk: vous pouvez gérer tous les effets secondaires sur les sagas tout en obtenant une promesse par dispatchingl'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)
    })
  }
}

1
l'utilisation à l' then()intérieur d'un composant React est contraire au paradigme. Vous devez gérer l'état modifié componentDidUpdateplutôt que d'attendre qu'une promesse soit résolue.

3
@ Maxincredible52 Ce n'est pas vrai pour le rendu côté serveur.
Diego Haz

D'après mon expérience, le point de Max est toujours vrai pour le rendu côté serveur. Cela devrait probablement être géré quelque part dans la couche de routage.
ThinkingInBits

3
@ Maxincredible52 pourquoi est-ce contre le paradigme, où avez-vous lu cela? Je fais généralement similaire à @Diego Haz mais le fais dans componentDidMount (selon les documents React, les appels réseau devraient de préférence être effectués là-bas), nous avons donccomponentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
user3711421

0

Un moyen plus simple consiste à utiliser redux-auto .

de la documantasion

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

  1. Pas besoin d'autre middleware asynchrone Redux. par exemple thunk, middleware de promesse, saga
  2. Vous permet facilement de passer une promesse dans redux et de la faire gérer pour vous
  3. Vous permet de co-localiser les 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 des données depuis le serveur au démarrage

L'idée est d'avoir chaque action dans un fichier spécifique . colocaliser l'appel du serveur dans le fichier avec les fonctions de réduction pour "en attente", "satisfait" et "rejeté". Cela rend la gestion des promesses très facile.

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


2
J'ai fait +1 même si c'est une réponse non pertinente parce que différentes solutions devraient également être envisagées
amorenew

12
Je pense que les-sont là parce qu'il n'a pas révélé qu'il est l'auteur du projet
jreptak
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.