Redux: Como encadear ações assíncronas?

Criado em 28 abr. 2016  ·  24Comentários  ·  Fonte: reduxjs/redux

Olá, tenho estudado Redux e me deparei com um problema interessante? precisa fazer uma cadeia de solicitações assíncronas de outras ações
1-getUsuário()
2-getPost()

Eu tenho 2 soluções executadas após o login do usuário .then(dispatch({type:GET_POST_REQUEST}))
ou escrever função no middleWare.

Como fazer corretamente?

docs

Comentários muito úteis

Oi! Este é um rastreador de problemas e não um fórum de suporte. Gostaríamos que você perguntasse no StackOverflow da próxima vez, porque as respostas aqui se perdem, ao contrário do SO.

Dito isso, se você criar sua loja com o middleware Redux Thunk , poderá escrever criadores de ações assíncronas 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 })

Devemos colocar isso no FAQ.

Todos 24 comentários

Oi! Este é um rastreador de problemas e não um fórum de suporte. Gostaríamos que você perguntasse no StackOverflow da próxima vez, porque as respostas aqui se perdem, ao contrário do SO.

Dito isso, se você criar sua loja com o middleware Redux Thunk , poderá escrever criadores de ações assíncronas 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 })

Devemos colocar isso no FAQ.

Resolvi este problema da seguinte forma. Sem modificar ações. Eu coloquei no 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 é a decisão correta?

Isso também funciona, qual padrão usar fica a seu critério.

@ar53n O padrão com uma promessa em um componente React tem várias falhas:

  • Não há tratamento de erros (no exemplo acima ), ou seja, a parte catch . Você pode receber rejeições não tratadas.
  • É ininterrupto, por exemplo, quando o componente é desmontado ou ocorre alguma ação que altera o estado do aplicativo.
  • Seu estado é implícito. Embora os efeitos colaterais sejam uma discussão separada, pelo menos ter um rastreamento desse processo em execução no estado do aplicativo seria útil.

@sompylasar obrigado John, obrigado por seus comentários. Eu só não quero modificar ações simples. Temos 2 ações simples Authentication e GetEvents são 2 requisições assíncronas e ainda contém catch . Apenas esta chamada de ação ao clicar no componente
por exemplo

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

E nós temos isso
image

Corrija-me se estiver errado, obrigado

@ar53n Então você está bem, os erros são tratados e o processo é rastreado na loja. O problema com o processo ininterrupto em um componente ainda se aplica, mas isso provavelmente não é tão importante se você iniciou em um componente ou em uma conversão de ação.

@ar53n Você também pode dar uma olhada no redux-dataloader .
Ele é projetado para ações assíncronas de cadeia complexas.
Espero que ajude!

Fechando isso, pois existem algumas soluções possíveis postadas aqui. Você também pode querer olhar para redux-saga hoje em dia!

@gaearon seu exemplo quebra o tempo de viagem, não é?

Quero dizer, por exemplo, sua chamada AJAX falhou na primeira vez e, em seguida, você corrige o lado do servidor e deseja refazê-lo

@gaearon Eu tentei sua solução, mas quando tento chamar store.dispatch(...) de qualquer componente (no meu caso de LoginComponent para fazer uma solicitação de autorização) recebi este erro:

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

Parece que algo está errado. Eu configurei o criador de ação assim:

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

o que estou perdendo?

@bm-software: Essa pergunta deve ser feita no Stack Overflow.

@ar53n e @sompylasar já faz um tempo, eu sei, mas estou lutando com esse padrão agora. @ar53n no seu exemplo, se o fetch dentro userAuth falhar, o que acontece na cadeia onde userAuth é chamado?

Parece que .catch(error => dispatch(errorUserAuth(error))) despacharia a ação errerUserAuth, o que é ótimo. No Redux, normalmente é assim que "tratamos" os erros. Mas na cadeia que você mencionou anteriormente:

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() _sempre_ será chamado, mesmo que a autenticação do usuário falhe. Eu não acho que é o que a maioria das pessoas esperaria ou desejaria, mas como o tratamento de erros do Redux geralmente é feito por despacho e não por relançamento, ele engole erros para que o encadeamento de promessas não funcione como esperado.

Além disso, se você lançar novamente, terá que .catch() em _toda chamada de criador de ação única em seu aplicativo, em todos os lugares_. O exemplo do @gaearon acima, onde ele lança novamente todos os erros, faz isso no final:

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

Se _qualquer coisa_ na grande cadeia combinada dentro getUserAndTheirFirstPost falhar, haverá um erro de "rejeição de promessa não tratada".

Acho que a única resposta é relançar e depois .catch() em todos os lugares, ou possivelmente usar os limites de erro do React 16.

@jasonrhodes

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

data.showEvents() _sempre_ será chamado, mesmo que a autenticação do usuário falhe.

Não, não será chamado se data.userAuth(data.login, data.password) retornar uma promessa que eventualmente será rejeitada. O primeiro argumento para .then é chamado quando a promessa é cumprida (chama-se onFulfilled ), o segundo quando a promessa é rejeitada (chama-se onRejected ) – veja a especificação .

Por outro lado, os limites de erro do React 16 não ajudam com promessas, eles apenas capturam exceções lançadas de forma síncrona para garantir que o estado interno do React não seja quebrado.

@jasonrhodes Além disso, seja um bom cidadão do mundo da Promise, ou retorne a promessa ao chamador (ele deve tratar os erros) ou anexe um .catch a ela (e trate os erros onde a promessa é criada).

Seu exemplo não faz 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 não foi meu exemplo, foi um que você respondeu anteriormente neste tópico. Eu sei que já faz um tempo, mas eu estava fazendo referência a conversas anteriores aqui porque me deparei com este tópico através de pesquisas no Google mais de uma vez.

Veja novamente, data.showEvents() será _sempre_ chamado mesmo se a chamada fetch dentro da função userAuth falhar. Por quê? Porque existe um .catch() dentro da função userAuth que trata o erro da maneira Redux: despachando uma ação de erro.

Este é o ponto do meu post: quando você pega erros de ações assíncronas para que você possa despachar ações de erro, você engole e impede que o encadeamento de promessas funcione corretamente? Ou você joga novamente e força todos os chamadores desse criador de ação a .catch() não importa o que aconteça, para evitar "rejeições de promessas não tratadas"?

Além disso, na minha experiência, descobri que é uma boa ideia entender o que alguém sabe antes de vinculá-los a uma especificação. Obrigado por responder, sei que é uma conversa antiga que desenterrei, mas é importante, pois você pode ver como é fácil perder!

@jasonrhodes

@sompylasar não foi meu exemplo, foi um que você respondeu anteriormente neste tópico. Eu sei que já faz um tempo, mas eu estava fazendo referência a conversas anteriores aqui porque me deparei com este tópico através de pesquisas no Google mais de uma vez.

Entendi, peço desculpas, não me lembrei de todo o contexto.

Porque existe um .catch() dentro da função userAuth que trata o erro da maneira Redux: despachando uma ação de erro.

Entendi, então isso funciona como pretendido, ou você não deve colocar .catch lá se você retornar essa promessa (e lidar com erros nos sites de chamada), ou você deve lançar novamente o erro dentro do .catch manipulador para rejeitar a promessa de downstream.

De qualquer forma, os próprios thunks não são adequados para encadeamento. Você deve encadear dentro de um thunk ou usar sagas para fluxos de trabalho assíncronos mais complexos.

Estou preso com esse problema de quebrar a cadeia de promessas por um tempo. Normalmente, meus criadores de ação emitiriam uma ação SUCCESS no .then() ou uma ação FAILURE no .catch() da mesma solicitação http que é retornada da conversão.

Sempre que meu criador de ação foi para o bloco catch e eu fiz this.props.myActionCreator().then(() => ) , o código dentro do then seria executado mesmo que houvesse problemas na solicitação.

Para explicar isso, sempre verifiquei se há uma variável de erro no estado do aplicativo definida no caso FALHA para esse criador de ação. Mas as coisas ficariam confusas quando você chamasse vários criadores de ação, especialmente aqueles que dependiam um do outro. Eu tive que ter uma instrução if verificando muitas variáveis ​​de erro.

Eu gosto do fato de não quebrar a cadeia de promessas ao relançar o erro no bloco catch para o valor de retorno do criador da ação. No entanto, isso exigiria que usássemos .catch() do componente React, onde o criador da ação é chamado. Eu não teria nada escrito nesse bloco catch, pois a variável error já está definida pelo tratamento da ação FAILURE em um redutor.

Então, vocês, @jasonrhodes , @sompylasar , me recomendariam usar a abordagem de relançamento e colocar um bloco .catch() vazio na cadeia de promessas de chamadas do criador de ação em um componente React?

@nbkhope para ser honesto, este tem sido o meu maior problema com o Redux e até hoje não descobri uma boa resposta. Desculpe não ser mais útil!

Pessoal, você pode encontrar algumas alternativas de conversão neste artigo https://decembersoft.com/posts/what-is-the-right-way-to-do-asynchronous-operations-in-redux/

@gaearon

Em relação à sua primeira resposta, como devo convertê-la se estiver usando async & await em vez de promessas? Algo como o seguinte:

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 você pode encadeá-los da seguinte forma:

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

@Boomxx Obrigado funciona como esperado.

Eu estou querendo saber como eu faria um fetch all posts of the user .

@km16 : Este é um rastreador de bugs, não um sistema de suporte. Para questões de uso, use Stack Overflow ou Reactiflux, onde há muito mais pessoas prontas para ajudá-lo - você provavelmente obterá uma resposta melhor mais rapidamente. Obrigado!

Esta página foi útil?
0 / 5 - 0 avaliações