575 votes

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

Il y a beaucoup de discussion à propos du dernier enfant dans redux ville, yelouafi/redux-saga. Il utilise le générateur de fonctions pour écouter/envoi actions.

Avant de m'envelopper ma tête autour de lui, j'aimerais connaître les avantages/inconvénients de l'utilisation de redux-saga , au lieu de l'approche ci-dessous où je suis à l'aide d' redux-thunk avec async/await.

Un composant peut ressembler à ceci, envoi 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);

Alors mes actes ressembler à 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...

505voto

Yassine Elouafi Points 1151

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 à remarquer est que nous allons appeler les fonctions de l'api à l'aide du formulaire yield call(func, ...args). call n'exécute pas l'effet, il crée un objet ordinaire comme {type: 'CALL', func, args}. L'exécution est déléguée à la redux-saga middleware qui prend soin de l'exécution de la fonction et de reprendre le générateur de son résultat.

Le principal avantage est que vous pouvez tester le générateur à l'extérieur de Redux à l'aide 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 sommes se moquant de l'api résultat de l'appel par simple injection de la moqué de données dans l' next méthode de l'itérateur. Se moquant de données est plus simple que de se moquant de fonctions.

La deuxième chose à remarquer, c'est l'appel à l' yield take(ACTION). Les Thunks sont appelés par l'action du créateur sur chaque nouvelle action (par exemple, LOGIN_REQUEST). c'est à dire les actions sont constamment poussés à thunks, et les thunks n'avons aucun contrôle sur le moment de cesser le traitement de ces actions.

Dans redux-saga, générateurs de tirer de la prochaine action. c'est à dire qu'ils ont le contrôle lors de l'écouter un peu d'action, et quand ne pas. Dans l'exemple ci-dessus, le flux d'instructions sont placées à l'intérieur d'un while(true) boucle, donc ça va écouter pour chaque nouvelle action, qui imite un peu le thunk poussant comportement.

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

  • Poignée de DÉCONNEXION action de l'utilisateur

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

  • Prendre en compte que lors de l'attente pour le résultat des appels d'api (soit votre première connexion ou actualiser) l'utilisateur peut déconnexion entre les deux.

Comment voulez-vous mettre en œuvre qu'avec les thunks; tout en fournissant de l'essai complet de la couverture de l'ensemble des flux? Voici comment il peut regarder 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 sommes en exprimant notre exigence de simultanéité à l'aide de race. Si take(LOGOUT) gagne la course (c'est à dire l'utilisateur a cliqué sur un Bouton de Déconnexion). La course entraînera automatiquement l'annulation de l' authAndRefreshTokenOnExpiry tâche de fond. Et si l' authAndRefreshTokenOnExpiry a été bloqué au milieu d'un call(authorize, {token}) appel il va également être annulée. L'annulation se propage à la baisse automatiquement.

Vous pouvez trouver un praticable de démonstration de la au-dessus de flux

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X