Redux: Comment enchaîner des actions asynchrones ?

Créé le 28 avr. 2016  ·  24Commentaires  ·  Source: reduxjs/redux

Bonjour, j'ai étudié Redux et j'ai été confronté à un problème intéressant ? besoin de faire une chaîne de requêtes asynchrones à partir d'autres actions
1-getUser()
2-getPost()

J'ai 2 solutions exécutées après la connexion de l'utilisateur .then(dispatch({type:GET_POST_REQUEST}))
ou fonction d'écriture dans le middleWare.

Comment faire correctement ?

docs

Commentaire le plus utile

Salut! Il s'agit d'un outil de suivi des problèmes et non d'un forum d'assistance. Nous vous serions reconnaissants de demander sur StackOverflow la prochaine fois car les réponses ici se perdent, contrairement à SO.

Cela dit, si vous créez votre boutique avec le middleware Redux Thunk , vous pouvez écrire des créateurs d'action asynchrones comme celui-ci :

// If you use Redux Thunk...
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(reducer, applyMiddleware(thunk))

// You can define asynchronous action creators that return functions.
// We call such action creators "thunks":

export function getUser(id) {
  // Redux Thunk will inject dispatch here:
  return dispatch => {
    // Reducers may handle this to set a flag like isFetching
    dispatch({ type: 'GET_USER_REQUEST', id })

    // Perform the actual API call
    return fetchUser().then(
      response => {
        // Reducers may handle this to show the data and reset isFetching
        dispatch({ type: 'GET_USER_SUCCESS', id,  response })
      },
      error => {
        // Reducers may handle this to reset isFetching
        dispatch({ type: 'GET_USER_FAILURE', id,  error })
        // Rethrow so returned Promise is rejected
        throw error
      }
    )
  }
}

// Thunks can be dispatched, if Redux Thunk is applied,
// just like normal action creators:
store.dispatch(getUser(42));

// The return value of dispatch() when you dispatch a thunk *is*
// the return value of the inner function. This is why it's useful
// to return a Promise (even though it is not strictly necessary):
store.dispatch(getUser(42)).then(() =>
  console.log('Fetched user and updated UI!')
)

// Here is another thunk action creator.
// It works exactly the same way.
export function getPost(id) {
  return dispatch => {
    dispatch({ type: 'GET_POST_REQUEST', id })
    return fetchPost().then(
      response => dispatch({ type: 'GET_POST_SUCCESS', id,  response }),
      error => {
        dispatch({ type: 'GET_POST_FAILURE', id,  error })
        throw error
      }
    )
  }
}

// Now we can combine them
export function getUserAndTheirFirstPost(userId) {
  // Again, Redux Thunk will inject dispatch here.
  // It also injects a second argument called getState() that lets us read the current state.
  return (dispatch, getState) => {
    // Remember I told you dispatch() can now handle thunks?
    return dispatch(getUser(userId)).then(() => {
      // Assuming this is where the fetched user got stored
      const fetchedUser = getState().usersById[userId]
      // Assuming it has a "postIDs" field:
      const firstPostID = fetchedUser.postIDs[0]
      // And we can dispatch() another thunk now!
      return dispatch(getPost(firstPostID))
    })
  }
}

// And we can now wait for the combined thunk:
store.dispatch(getUserAndTheirFirstPost(43)).then(() => {
  console.log('fetched a user and their first post')
})

// We can do this anywhere we have access to dispatch().
// For example, we can use this.props.dispatch, or put action
// creators right into the props by passing them to connect, like this:
// export default connect(mapStateToProps, { getUserAndTheirFirstPost })

Nous devrions mettre cela dans la FAQ.

Tous les 24 commentaires

Salut! Il s'agit d'un outil de suivi des problèmes et non d'un forum d'assistance. Nous vous serions reconnaissants de demander sur StackOverflow la prochaine fois car les réponses ici se perdent, contrairement à SO.

Cela dit, si vous créez votre boutique avec le middleware Redux Thunk , vous pouvez écrire des créateurs d'action asynchrones comme celui-ci :

// If you use Redux Thunk...
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(reducer, applyMiddleware(thunk))

// You can define asynchronous action creators that return functions.
// We call such action creators "thunks":

export function getUser(id) {
  // Redux Thunk will inject dispatch here:
  return dispatch => {
    // Reducers may handle this to set a flag like isFetching
    dispatch({ type: 'GET_USER_REQUEST', id })

    // Perform the actual API call
    return fetchUser().then(
      response => {
        // Reducers may handle this to show the data and reset isFetching
        dispatch({ type: 'GET_USER_SUCCESS', id,  response })
      },
      error => {
        // Reducers may handle this to reset isFetching
        dispatch({ type: 'GET_USER_FAILURE', id,  error })
        // Rethrow so returned Promise is rejected
        throw error
      }
    )
  }
}

// Thunks can be dispatched, if Redux Thunk is applied,
// just like normal action creators:
store.dispatch(getUser(42));

// The return value of dispatch() when you dispatch a thunk *is*
// the return value of the inner function. This is why it's useful
// to return a Promise (even though it is not strictly necessary):
store.dispatch(getUser(42)).then(() =>
  console.log('Fetched user and updated UI!')
)

// Here is another thunk action creator.
// It works exactly the same way.
export function getPost(id) {
  return dispatch => {
    dispatch({ type: 'GET_POST_REQUEST', id })
    return fetchPost().then(
      response => dispatch({ type: 'GET_POST_SUCCESS', id,  response }),
      error => {
        dispatch({ type: 'GET_POST_FAILURE', id,  error })
        throw error
      }
    )
  }
}

// Now we can combine them
export function getUserAndTheirFirstPost(userId) {
  // Again, Redux Thunk will inject dispatch here.
  // It also injects a second argument called getState() that lets us read the current state.
  return (dispatch, getState) => {
    // Remember I told you dispatch() can now handle thunks?
    return dispatch(getUser(userId)).then(() => {
      // Assuming this is where the fetched user got stored
      const fetchedUser = getState().usersById[userId]
      // Assuming it has a "postIDs" field:
      const firstPostID = fetchedUser.postIDs[0]
      // And we can dispatch() another thunk now!
      return dispatch(getPost(firstPostID))
    })
  }
}

// And we can now wait for the combined thunk:
store.dispatch(getUserAndTheirFirstPost(43)).then(() => {
  console.log('fetched a user and their first post')
})

// We can do this anywhere we have access to dispatch().
// For example, we can use this.props.dispatch, or put action
// creators right into the props by passing them to connect, like this:
// export default connect(mapStateToProps, { getUserAndTheirFirstPost })

Nous devrions mettre cela dans la FAQ.

J'ai résolu ce problème comme suit. Sans modifier les actions. J'ai mis le composant Promise.

  clickShowUserEvent(data) {
   Promise.resolve(data.userAuth(data.login, data.password)) // dispatch
    .then(function (response) {
      data.showEvents(); //dispatch
      return response;
    })
    .then(function(response){console.log("@RESPONSE",response);data.show(data)})
  }

C'est la bonne décision ?

Cela fonctionne aussi, le modèle à utiliser dépend de vous.

@ar53n Le modèle avec une promesse dans un composant React présente plusieurs défauts :

  • Il n'y a pas de gestion des erreurs (dans l' exemple ci-dessus ), c'est-à-dire la partie catch . Vous pouvez obtenir des rejets non gérés.
  • Il est ininterruptible, par exemple lorsque le composant se démonte ou qu'une action se produit qui modifie l'état de l'application.
  • Son état est implicite. Bien que les effets secondaires soient une discussion distincte, il serait utile d'avoir au moins une trace de ce processus en cours d'exécution dans l'état de l'application.

@sompylasar merci John merci pour vos commentaires. Je ne veux tout simplement pas modifier des actions simples. Nous avons 2 actions simples Authentication et GetEvents il s'agit de 2 requêtes asynchrones et contiennent même catch . Juste cet appel d'action lorsque vous cliquez sur le composant
par exemple

export function userAuth(login, password) {
  return (dispatch, getState) => {
    console.log('STATE', getState())
    let newState = dispatch(requestUserAuth(login, password))
    return fetch(AUTH_URL + newState.queryParams, MY_INIT)
      .then(response => response.json())
      .then(function (json) { dispatch(receiveUserAuth(json)); return json})
      .catch(error => dispatch(errorUserAuth(error)))
  }
}

Et nous avons ceci
image

Corrigez-moi si je me trompe, merci

@ ar53n Alors tout va bien, les erreurs sont gérées et le processus est suivi dans le magasin. Le problème avec le processus sans interruption dans un composant s'applique toujours, mais ce n'est probablement pas si important si vous avez commencé dans un composant ou dans une action thunk.

@ ar53n Vous pouvez également jeter un œil à redux-dataloader .
Il est conçu pour les actions asynchrones en chaîne complexes.
J'espère que ça aide!

Clôture car il y a des solutions possibles postées ici. Vous voudrez peut-être aussi vous pencher sur redux-saga de nos jours !

@gaearon est-ce que ton exemple casse le temps de voyager n'est-ce pas ?

Je veux dire, par exemple, votre appel AJAX a échoué la première fois, puis vous allez réparer le côté serveur et vous voulez le refaire

@gaearon j'ai essayé votre solution mais lorsque j'essaie d'appeler store.dispatch(...) depuis n'importe quel composant (dans mon cas depuis LoginComponent pour faire une demande d'autorisation), j'ai cette erreur :

undefined is not an object (evaluating '_AppNavigator.AppNavigator.router')

Il semble que quelque chose ne va pas. J'ai configuré le créateur d'action comme ceci:

// actions.tsx
export const ActionCreators = {
    authenticate: (username: string, password: string) => {
        return (dispatch) => {
            return auth.login(username, password).then(
                response => {
                    dispatch(navActionCreators.login(res))
                    return response
                },
                error => {
                    throw error
                }
            )
        }
    }
}

// LoginScreen.tsx (login method)
store.dispatch(authActions.authenticate(this.state.username, this.state.password))
  .then((res) => {
     this.setState({isLoading: false})
   })
   .catch((error: Error) => {
     this.setState({
       isLoading: false,
       error: error ? error.message : 'Si è verificato un\' errore.'
     })
   })

Qu'est-ce que je rate?

@bm-software : cette question devrait plutôt être posée sur Stack Overflow.

@ar53n et @sompylasar ça fait un moment que je sais, mais je me bats avec ce schéma en ce moment. @ ar53n dans votre exemple, si le fetch à l'intérieur userAuth échoue, que se passe-t-il dans la chaîne où userAuth est appelé ?

Il semble que .catch(error => dispatch(errorUserAuth(error))) envoie l'action errerUserAuth, ce qui est génial. Dans Redux, c'est généralement ainsi que nous "gérons" les erreurs. Mais dans la chaîne que vous avez mentionnée plus tôt :

clickShowUserEvent(data) {
   Promise.resolve(data.userAuth(data.login, data.password)) // dispatch
    .then(function (response) {
      data.showEvents(); //dispatch
      return response;
    })
    .then(function(response){console.log("@RESPONSE",response);data.show(data)})
  }

data.showEvents() sera _toujours_ appelé, même si l'authentification de l'utilisateur échoue. Je ne pense pas que ce soit ce à quoi la plupart des gens s'attendraient ou voudraient, mais comme la gestion des erreurs Redux se fait généralement par répartition et non par relance, elle avale les erreurs de sorte que le chaînage des promesses ne fonctionne pas comme prévu.

De plus, si vous relancez, vous êtes obligé de .catch() sur _chaque appel de créateur d'action dans votre application, partout_. L'exemple de @gaearon ci-dessus, où il renvoie toutes les erreurs, le fait à la fin :

// And we can now wait for the combined thunk:
store.dispatch(getUserAndTheirFirstPost(43)).then(() => {
  console.log('fetched a user and their first post')
})

Si _n'importe quoi_ dans la grande chaîne combinée à l'intérieur getUserAndTheirFirstPost échoue, il y aura une erreur "Rejet de promesse non gérée".

Je pense que la seule réponse est de relancer puis .catch() partout, ou éventuellement d'utiliser les limites d'erreur React 16.

@jasonrhodes

clickShowUserEvent(data) {
   Promise.resolve(data.userAuth(data.login, data.password)) // dispatch
    .then(function (response) {
      data.showEvents(); //dispatch

data.showEvents() sera _toujours_ appelé, même si l'authentification de l'utilisateur échoue.

Non, il ne sera pas appelé si data.userAuth(data.login, data.password) renvoie une promesse qui est finalement rejetée. Le premier argument de .then est appelé lorsque la promesse est remplie (il s'appelle onFulfilled ), le second lorsque la promesse est rejetée (il s'appelle onRejected ) – voir la spécification .

D'un autre côté, les limites d'erreur React 16 n'aideront pas avec les promesses, elles interceptent uniquement les exceptions lancées de manière synchrone pour garantir que l'état interne de React n'est pas rompu.

@jasonrhodes De plus, soyez un bon citoyen du monde Promise, soit renvoyez la promesse à l'appelant (il doit alors gérer les erreurs), soit attachez-lui un .catch (et gérez les erreurs là où la promesse est créée).

Votre exemple ne fait rien:

clickShowUserEvent(data) {
   Promise.resolve(data.userAuth(data.login, data.password)) // dispatch
    .then(function (response) {
      data.showEvents(); //dispatch
      return response;
    })
    .then(function(response){console.log("@RESPONSE",response);data.show(data)})
  }

@sompylasar ce n'était pas mon exemple, c'était celui auquel vous aviez répondu plus tôt dans ce fil. Je sais que cela fait un moment, mais je faisais référence à des conversations précédentes ici parce que j'ai rencontré ce fil via des recherches Google plus d'une fois.

Regardez encore, data.showEvents() sera _toujours_ appelé même si l'appel fetch à l'intérieur de la fonction userAuth échoue. Pourquoi? Parce qu'il y a un .catch() à l'intérieur de la fonction userAuth qui gère l'erreur à la manière Redux : en envoyant une action d'erreur.

C'est le but de mon message : lorsque vous détectez des erreurs provenant d'actions asynchrones afin de pouvoir envoyer des actions d'erreur, avalez-vous et empêchez-vous que le chaînage de promesses ne fonctionne correctement ? Ou relancez-vous et forcez-vous chaque appelant de ce créateur d'action à .catch() quoi qu'il arrive pour éviter les "rejets de promesses non gérés" ?

De plus, d'après mon expérience, j'ai trouvé que c'était une bonne idée de comprendre ce que quelqu'un sait avant de le lier à une spécification. Merci d'avoir répondu, je sais que c'est une vieille conversation que j'ai déterrée, mais c'est important car vous pouvez voir à quel point il est facile de la manquer !

@jasonrhodes

@sompylasar ce n'était pas mon exemple, c'était celui auquel vous aviez répondu plus tôt dans ce fil. Je sais que cela fait un moment, mais je faisais référence à des conversations précédentes ici parce que j'ai rencontré ce fil via des recherches Google plus d'une fois.

J'ai compris, je m'excuse, je n'ai pas rappelé tout le contexte.

Parce qu'il y a un .catch() à l'intérieur de la fonction userAuth qui gère l'erreur à la manière Redux : en envoyant une action d'erreur.

J'ai compris, alors soit cela fonctionne comme prévu, soit vous ne devriez pas y mettre .catch si vous renvoyez cette promesse (et gérez les erreurs sur les sites d'appel), ou vous devriez relancer l'erreur dans le .catch pour rejeter la promesse en aval.

Quoi qu'il en soit, les thunks eux-mêmes ne sont pas bien adaptés au chaînage. Vous devez enchaîner dans un thunk ou utiliser des sagas pour des flux de travail asynchrones plus complexes.

J'ai été coincé avec ce problème de briser la chaîne de la promesse pendant un moment. Habituellement, mes créateurs d'action émettraient une action SUCCESS dans le .then() ou une action FAILURE dans le .catch() de cette même requête http renvoyée par le thunk.

Chaque fois que mon créateur d'action est allé au bloc catch et que j'ai fait this.props.myActionCreator().then(() => ) , le code à l'intérieur du then s'exécuterait même s'il y avait des problèmes dans la requête.

Pour tenir compte de cela, je me suis toujours assuré de rechercher une variable d'erreur dans l'état de l'application qui est définie dans le cas FAILURE pour ce créateur d'action. Mais les choses devenaient compliquées lorsque vous appeliez plusieurs créateurs d'action, en particulier ceux qui dépendaient les uns des autres. Je devais avoir une instruction if vérifiant de nombreuses variables d'erreur.

J'aime le fait de ne pas casser la chaîne de promesses en renvoyant l'erreur dans le bloc catch pour la valeur de retour du créateur d'action. Cependant, cela nous obligerait à utiliser .catch() du composant React, où le créateur de l'action est appelé. Je n'aurais rien écrit dans ce bloc catch, car la variable d'erreur est déjà définie par la gestion de l'action FAILURE dans un réducteur.

Alors, les gars, @jasonrhodes , @sompylasar , me recommanderiez-vous d'utiliser l'approche de relance et de placer un bloc .catch() vide sur la chaîne de promesses d'appels du créateur d'action dans un composant React ?

@nbkhope pour être honnête, cela a été mon plus gros problème avec Redux et à ce jour, je n'ai pas trouvé de bonne réponse. Désolé de ne pas être plus utile !

Les gars, vous pouvez trouver des alternatives thunk dans cet article https://decembersoft.com/posts/what-is-the-right-way-to-do-asynchronous-operations-in-redux/

@gaearon

En ce qui concerne votre première réponse, comment dois-je la convertir si j'utilise async & wait au lieu de promesses ? Quelque chose comme ce qui suit :

export const funcA = () => {
    return async (dispatch) => {
        const data = await doSomething(...)
        dispatch({ action: DID_SOMETHING, payload: data })
    }
}

export const funcB = () => {
    return async (dispatch) => {
        const data = await doSomethingElse(...)
        dispatch({ action: DID_SOMETHING_ELSE, payload: data })
    }
}

export const funcC = () => {
    return async (dispatch) => {
        const data = await doSomethingMore(...)
        dispatch({ action: DID_SOMETHING_MORE, payload: data })
    }
}

// how to chain funcA, funcB and funcC
const myFunc = () => {
    // execute funcA
    // when complete execute funcB
    // when complete execute funcC
}

@yingdongzhang vous pouvez les enchaîner comme suit :

const myFunc = () => {
  return async (dispatch) => {
    try {
      await dispatch(funcA())
      await dispatch(funcB())
      await dispatch(funcC())
    } catch (error) {
      //error handling
    }
  }
}

@Boomxx Merci fonctionne comme prévu.

Je me demande comment ferais-je un fetch all posts of the user .

@km16 : Il s'agit d'un outil de suivi des bogues, pas d'un système d'assistance. Pour les questions d'utilisation, veuillez utiliser Stack Overflow ou Reactiflux où il y a beaucoup plus de personnes prêtes à vous aider - vous obtiendrez probablement une meilleure réponse plus rapidement. Merci!

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

mickeyreiss-visor picture mickeyreiss-visor  ·  3Commentaires

caojinli picture caojinli  ·  3Commentaires

ilearnio picture ilearnio  ·  3Commentaires

captbaritone picture captbaritone  ·  3Commentaires

olalonde picture olalonde  ·  3Commentaires