Redux: ¿Cómo encadenar acciones asíncronas?

Creado en 28 abr. 2016  ·  24Comentarios  ·  Fuente: reduxjs/redux

Hola, he estado estudiando Redux y me enfrenté a un problema interesante? necesita hacer una cadena de solicitudes asincrónicas de otras acciones
1-obtenerUsuario()
2-getPost()

Tengo 2 soluciones ejecutadas después de iniciar sesión en el usuario .then(dispatch({type:GET_POST_REQUEST}))
o escribir la función en middleWare.

¿Cómo hacerlo correctamente?

docs

Comentario más útil

¡Hola! Este es un rastreador de problemas y no un foro de soporte. Le agradeceríamos que preguntara en StackOverflow la próxima vez porque las respuestas aquí se pierden, a diferencia de SO.

Dicho esto, si crea su tienda con el middleware Redux Thunk , puede escribir creadores de acciones asincrónicas como este:

// 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 })

Deberíamos poner esto en las preguntas frecuentes.

Todos 24 comentarios

¡Hola! Este es un rastreador de problemas y no un foro de soporte. Le agradeceríamos que preguntara en StackOverflow la próxima vez porque las respuestas aquí se pierden, a diferencia de SO.

Dicho esto, si crea su tienda con el middleware Redux Thunk , puede escribir creadores de acciones asincrónicas como este:

// 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 })

Deberíamos poner esto en las preguntas frecuentes.

Resolví este problema de la siguiente manera. Sin modificar acciones. Puse el componente 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)})
  }

Esta es la decisión correcta?

Esto también funciona, qué patrón usar depende de usted.

@ar53n El patrón con una promesa en un componente React tiene varios defectos:

  • No hay manejo de errores (en el ejemplo anterior ), es decir, la parte catch . Es posible que obtenga rechazos no controlados.
  • Es ininterrumpible, por ejemplo, cuando el componente se desmonta o ocurre alguna acción que cambia el estado de la aplicación.
  • Su estado es implícito. Aunque los efectos secundarios son una discusión separada, al menos sería útil tener un rastro de ese proceso en ejecución en el estado de la aplicación.

@sompylasar gracias John gracias por tus comentarios. Simplemente no quiero modificar acciones simples. Tenemos 2 acciones simples Authentication y GetEvents son 2 solicitudes asíncronas e incluso contienen catch . Solo esta llamada de acción al hacer clic en el componente
p.ej

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)))
  }
}

Y tenemos esto
image

corrígeme si me equivoco, gracias

@ ar53n Entonces está bien, los errores se manejan y el proceso se rastrea en la tienda. El problema con el proceso ininterrumpido en un componente aún se aplica, pero eso probablemente no sea tan importante si ha comenzado en un componente o en un procesador de acción.

@ ar53n También puede echar un vistazo a redux-dataloader .
Está diseñado para acciones complejas asíncronas en cadena.
¡Espero eso ayude!

Cerrando esto ya que hay algunas posibles soluciones publicadas aquí. ¡Es posible que también desee ver redux-saga hoy en día!

@gaearon , ¿tu ejemplo rompe el viaje en el tiempo, verdad?

Quiero decir, por ejemplo, su llamada AJAX falló la primera vez y luego va a arreglar el lado del servidor y quiere rehacerlo

@gaearon Probé su solución, pero cuando intento llamar a store.dispatch(...) desde cualquier componente (en mi caso, desde LoginComponent para realizar una solicitud de autorización), aparece este error:

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

Parece que algo anda mal. He configurado el creador de acciones de esta manera:

// 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é me estoy perdiendo?

@bm-software: esa pregunta debería hacerse en Stack Overflow.

@ar53n y @sompylasar ha pasado un tiempo, lo sé, pero estoy luchando con este patrón en este momento. @ar53n en su ejemplo, si el fetch dentro userAuth falla, ¿qué sucede en la cadena donde se llama userAuth ?

Parece que .catch(error => dispatch(errorUserAuth(error))) enviaría la acción errerUserAuth, lo cual es genial. En Redux, esta es típicamente la forma en que "manejamos" los errores. Pero en la cadena que mencionaste antes:

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() _siempre_ se llamará, incluso si falla la autenticación del usuario. No creo que eso sea lo que la mayoría de la gente esperaría o desearía, pero dado que el manejo de errores de Redux generalmente se realiza despachando y no volviendo a lanzar, se traga los errores para que el encadenamiento de promesas no funcione como se esperaba.

Además, si vuelves a lanzar, tendrás que pagar .catch() en _todas las llamadas del creador de acción en tu aplicación, en todas partes_. El ejemplo de @gaearon arriba, donde vuelve a arrojar todos los errores, hace esto al final:

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

Si _cualquier cosa_ en la gran cadena combinada dentro getUserAndTheirFirstPost falla, habría un error de "Rechazo de promesa no manejado".

Creo que la única respuesta es volver a lanzar y luego .catch() todas partes, o posiblemente usar los límites de error de React 16.

@jasonrhodes

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

data.showEvents() _siempre_ se llamará, incluso si falla la autenticación del usuario.

No, no se llamará si data.userAuth(data.login, data.password) devuelve una promesa que finalmente se rechaza. El primer argumento de .then se llama cuando se cumple la promesa (se llama onFulfilled ), el segundo cuando se rechaza la promesa (se llama onRejected ) – ver la especificación .

Por otro lado, los límites de error de React 16 no ayudarán con las promesas, solo detectan excepciones lanzadas sincrónicamente para garantizar que el estado interno de React no se rompa.

@jasonrhodes Además, sea un buen ciudadano del mundo Promise, devuelva la promesa a la persona que llama (luego debe manejar los errores) o adjunte un .catch (y maneje los errores donde se crea la promesa).

Tu ejemplo no hace nada:

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 no era mi ejemplo, era uno al que habías estado respondiendo anteriormente en este hilo. Sé que ha pasado un tiempo, pero me refería a conversaciones anteriores aquí porque me he encontrado con este hilo a través de búsquedas en Google más de una vez.

Mire de nuevo, data.showEvents() _siempre_ se llamará incluso si falla la llamada a fetch dentro de la función userAuth . ¿Por qué? Porque hay un .catch() dentro de la función userAuth que maneja el error al estilo de Redux: enviando una acción de error.

Este es el punto de mi publicación: cuando detecta errores de acciones asíncronas para poder enviar acciones de error, ¿traga y evita que el encadenamiento de promesas funcione correctamente? ¿O vuelves a lanzar y obligas a cada persona que llama a ese creador de acción a .catch() sin importar qué para evitar "rechazos de promesa no manejados"?

Además, en mi experiencia, descubrí que es una buena idea comprender lo que alguien sabe antes de vincularlo a una especificación. Gracias por responder, sé que es una conversación antigua que he desenterrado, pero es importante, ya que puedes ver lo fácil que es perderse.

@jasonrhodes

@sompylasar no era mi ejemplo, era uno al que habías estado respondiendo anteriormente en este hilo. Sé que ha pasado un tiempo, pero me refería a conversaciones anteriores aquí porque me he encontrado con este hilo a través de búsquedas en Google más de una vez.

Lo tengo, me disculpo, no he recordado todo el contexto.

Porque hay un .catch() dentro de la función userAuth que maneja el error a la manera de Redux: enviando una acción de error.

Entendido, entonces esto funciona según lo previsto, o no debe poner .catch allí si devuelve esa promesa (y maneja los errores en los sitios de llamadas), o debe volver a generar el error dentro de .catch controlador

De todos modos, los propios thunks no son adecuados para encadenarlos. Debe encadenar dentro de un thunk o usar sagas para flujos de trabajo asincrónicos más complejos.

He estado atrapado con ese problema de romper la cadena de promesas por un tiempo. Por lo general, mis creadores de acciones emitirían una acción SUCCESS en .then() o una acción FAILURE en .catch() de esa misma solicitud http que se devuelve desde el thunk.

Cada vez que mi creador de acciones fue al bloque catch e hice this.props.myActionCreator().then(() => ) , el código dentro de then se ejecutaría incluso si hubiera problemas en la solicitud.

Para dar cuenta de eso, siempre me aseguré de buscar una variable de error en el estado de la aplicación que se establece en el caso de FALLA para el creador de esa acción. Pero las cosas se complicarían cuando llamaras a múltiples creadores de acciones, especialmente a los que dependían unos de otros. Tenía que tener una instrucción if para verificar muchas variables de error.

Me gusta el hecho de no romper la cadena de promesa al volver a lanzar el error en el bloque catch para el valor de retorno del creador de la acción. Sin embargo, eso requeriría que usemos .catch() del componente React, donde se llama al creador de la acción. No tendría nada escrito en ese bloque catch, ya que la variable de error ya está configurada por el manejo de la acción FAILURE en un reductor.

Entonces, chicos, @jasonrhodes , @sompylasar , ¿me recomendarían usar el enfoque de volver a lanzar y colocar un bloque .catch() vacío en la cadena de promesa de llamadas del creador de acción en un componente React?

@nbkhope , para ser honesto, este ha sido mi mayor problema con Redux y hasta el día de hoy no he encontrado una buena respuesta. ¡Lamento no ser más útil!

Chicos, pueden encontrar algunas alternativas thunk en este artículo https://decembersoft.com/posts/what-is-the-right-way-to-do-asynchronous-operations-in-redux/

@gaearon

Con respecto a su primera respuesta, ¿cómo debo convertirla si estoy usando async & await en lugar de promesas? Algo como lo siguiente:

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 puedes encadenarlos de la siguiente manera:

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

@Boomxx Gracias funciona como se esperaba.

Me pregunto cómo haría un fetch all posts of the user .

@km16 : Este es un rastreador de errores, no un sistema de soporte. Para preguntas de uso, use Stack Overflow o Reactiflux donde hay muchas más personas listas para ayudarlo; probablemente obtendrá una mejor respuesta más rápido. ¡Gracias!

¿Fue útil esta página
0 / 5 - 0 calificaciones