Avantages et inconvénients de l'utilisation de redux-saga avec des générateurs ES6 vs redux-thunk avec ES2017 async/wait

on parle beaucoup du dernier enfant de redux town en ce moment, redux-saga/redux-saga . Il utilise des fonctions de générateur pour l'écoute et l'envoi d'actions.

avant d'y réfléchir, j'aimerais connaître les avantages et les inconvénients de l'utilisation de redux-saga au lieu de l'approche ci-dessous où j'utilise redux-thunk avec async/attente.

un composant pourrait ressembler à ça, des actions de répartition 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);

Alors mes actions ressemblent à quelque chose comme 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...
380
demandé sur Evaldas Buinauskas 2016-01-21 20:45:12

7 réponses

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) . 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 à l'middleware redux-saga qui s'occupe de l'exécution de la fonction et de la reprise du générateur avec son résultat.

le principal avantage est-ce que vous pouvez tester le générateur à L'extérieur de Redux en utilisant des contrôles d'égalité simples

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 moquées dans la méthode next de l'itérateur. Se moquer des données est bien plus simple que se moquer des fonctions.

la deuxième chose à noter est l'appel à yield take(ACTION) . Les malles sont appelées par le créateur d'action sur chaque nouvelle action (par exemple LOGIN_REQUEST ). c'est à dire les actions sont continuellement a poussé vers les malles, et les malles n'ont aucun contrôle sur quand arrêter de manipuler ces actions.

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

le 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 la DÉCONNEXION de l'utilisateur de l'action

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

  • prendre en compte que, en attendant le résultat des appels api (soit la connexion initiale ou rafraîchir) l'utilisateur peut se déconnecter entre les deux.

comment mettriez-vous cela en œuvre avec des malles; tout en fournissant une couverture de test complète pour l'ensemble du flux? Voici à quoi ça ressemble avec 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 (c.-à-d. l'Utilisateur a cliqué sur un bouton de déconnexion). La course annulera automatiquement la tâche authAndRefreshTokenOnExpiry . Et si le authAndRefreshTokenOnExpiry a été bloqué au milieu d'un appel call(authorize, {token}) , il sera également annulé. L'Annulation se propage automatiquement vers le bas.

Vous pouvez trouver un praticable démo de ce qui précède flux

389
répondu Yassine Elouafi 2016-08-11 15:50:25

je vais ajouter mon expérience en utilisant saga dans le système de production en plus de la réponse plutôt approfondie de l'auteur de la bibliothèque.

Pro (à l'aide de la saga):

  • testabilité. Il est très facile de tester les sagas quand call() renvoie un objet pur. Testing thunks exige normalement que vous incluiez un mockStore dans votre test.

  • redux-saga est livré avec beaucoup de fonctions utiles helper environ tâche. Il me semble que le concept de saga est de créer une sorte de worker/thread d'arrière-plan pour votre application, qui agit comme une pièce manquante dans l'architecture react redux(actionCreators et réducteurs doivent être des fonctions pures.) Ce qui nous amène au point suivant.

  • Sagas offrent un endroit indépendant pour gérer tous les effets secondaires. D'après mon expérience, il est généralement plus facile de modifier et de gérer que les actions de thunk.

Con:

  • syntaxe de générateur.

  • beaucoup de concepts à apprendre.

  • API stability. Il semble que redux-saga ajoute encore des fonctionnalités (par exemple des canaux? et la communauté n'est pas aussi grand. Il y a un problème si la bibliothèque fait une mise à jour non compatible en arrière un jour.

78
répondu yjcxy12 2016-06-22 16:20:05

je voudrais juste ajouter quelques commentaires de mon expérience personnelle (en utilisant à la fois sagas et thunk):

Sagas sont d'une grande épreuve:

  • Vous n'avez pas besoin de se moquer des fonctions enveloppé avec des effets
  • donc les tests sont propres, lisibles et faciles à écrire
  • lors de l'utilisation de sagas, les créateurs d'action rendent la plupart du temps des objets simples littéraux. Il est également plus facile de tester et d'affirmer contrairement à promettre.
  • Les Sagas

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

  • attendez une action/des actions à être expédié ( take )
  • annuler la routine existante( cancel , takeLatest , race )
  • plusieurs routines peuvent écouter la même action ( take , takeEvery ,...)

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

  • channels écouter sur des sources d'événements externes (p.ex. des sites Web)
  • modèle fourche( fork , spawn )
  • gaz
  • ...

Sagas sont grand et puissant outil. Cependant, 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 envoyée, ou ce qui se passe quand une action est envoyée. D'autre part thunk est plus simple et plus facile à comprendre. 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 la préférence de l'équipe de développement. Dans tous les cas, il suffit de garder votre application simple et prévisible.

18
répondu madox2 2018-01-29 08:58:21

ayant passé en revue quelques différents projets à grande échelle React/Redux dans mon expérience Sagas fournir aux développeurs une façon plus structurée d'écrire du code qui est beaucoup plus facile à tester et plus difficile à se tromper.

Oui, c'est un peu bizarre au début, mais la plupart des devs obtenir assez de la compréhension d'une journée. Je dis toujours aux gens de ne pas se soucier de ce que yield fait pour commencer et qu'une fois que vous écrivez un couple de test, il viendra à vous.

j'ai vu un couple de projets où thunks ont été traités comme s'ils étaient des contrôleurs du MVC patten et cela devient rapidement un gâchis incontournable.

mon conseil est D'utiliser Sagas où vous avez besoin d'un déclencheurs de type B choses relatives à un seul événement. Pour tout ce qui pourrait traverser un certain nombre d'actions, je trouve qu'il est plus simple d'écrire un middleware client et d'utiliser la propriété meta d'une action FSA pour le déclencher.

0
répondu David Bradshaw 2018-06-14 21:04:03

voici un projet qui combine les meilleures parties (pros) des deux redux-saga et redux-thunk : vous pouvez gérer tous les effets secondaires sur les sagas tout en obtenant une promesse par 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)
    })
  }
}
-1
répondu Diego Haz 2017-05-23 03:39:00

une façon plus facile est d'utiliser redux-auto .

de la documentation

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

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

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

il attache aussi automatiquement un objet helper(appelé" async") au prototype de votre état, vous permettant de suivre dans votre UI, transitions demandées.

-1
répondu codemeasandwich 2017-06-24 13:25:10

une petite note. Les générateurs sont annulables, async / wait-not. Donc, pour un exemple tiré de la question, cela n'a pas vraiment de sens de choisir quoi. 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 avec Redux-thunk, mais pour moi, il semble comme essayer d'inventer un vélo avec des roues carrées.

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

-2
répondu Dmitriy 2018-06-14 22:11:29