Não caia na armadilha de pensar que uma biblioteca deve prescrever como fazer tudo . Se você quiser fazer algo com um tempo limite em JavaScript, precisará usar setTimeout
. Não há razão para que as ações do Redux sejam diferentes.
Redux não oferecer algumas formas alternativas de lidar com coisas assíncrona, mas você só deve usar esses quando você percebe que você está repetindo muito código. A menos que você tenha esse problema, use o que o idioma oferece e procure a solução mais simples.
Escrevendo código assíncrono em linha
Esta é de longe a maneira mais simples. E não há nada específico para o Redux aqui.
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Da mesma forma, de dentro de um componente conectado:
this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
A única diferença é que, em um componente conectado, você geralmente não tem acesso à própria loja, mas obtém um dispatch()
ou mais criadores de ações específicas injetados como acessórios. No entanto, isso não faz nenhuma diferença para nós.
Se você não gosta de digitar erros ao despachar as mesmas ações de componentes diferentes, convém extrair criadores de ação em vez de despachar objetos de ação em linha:
// actions.js
export function showNotification(text) {
return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
return { type: 'HIDE_NOTIFICATION' }
}
// component.js
import { showNotification, hideNotification } from '../actions'
this.props.dispatch(showNotification('You just logged in.'))
setTimeout(() => {
this.props.dispatch(hideNotification())
}, 5000)
Ou, se você os vinculou anteriormente a connect()
:
this.props.showNotification('You just logged in.')
setTimeout(() => {
this.props.hideNotification()
}, 5000)
Até agora, não usamos nenhum middleware ou outro conceito avançado.
Extraindo criador de ação assíncrona
A abordagem acima funciona bem em casos simples, mas você pode achar que tem alguns problemas:
- Obriga você a duplicar essa lógica em qualquer lugar que desejar mostrar uma notificação.
- As notificações não têm IDs, portanto você terá uma condição de corrida se mostrar duas notificações com rapidez suficiente. Quando o primeiro tempo limite terminar, ele será despachado
HIDE_NOTIFICATION
, ocultando erroneamente a segunda notificação mais cedo do que após o tempo limite.
Para resolver esses problemas, você precisaria extrair uma função que centralize a lógica de tempo limite e despache essas duas ações. Pode ser assim:
// actions.js
function showNotification(id, text) {
return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
return { type: 'HIDE_NOTIFICATION', id }
}
let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
// Assigning IDs to notifications lets reducer ignore HIDE_NOTIFICATION
// for the notification that is not currently visible.
// Alternatively, we could store the timeout ID and call
// clearTimeout(), but we’d still want to do it in a single place.
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
Agora os componentes podem usar showNotificationWithTimeout
sem duplicar essa lógica ou ter condições de corrida com notificações diferentes:
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Por que showNotificationWithTimeout()
aceitadispatch
como o primeiro argumento? Porque ele precisa despachar ações para a loja. Normalmente, um componente tem acesso, dispatch
mas, como queremos que uma função externa assuma o controle sobre o envio, precisamos fornecer controle sobre o envio.
Se você exportou uma loja singleton de algum módulo, basta importá-la e dispatch
diretamente nela:
// store.js
export default createStore(reducer)
// actions.js
import store from './store'
// ...
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
const id = nextNotificationId++
store.dispatch(showNotification(id, text))
setTimeout(() => {
store.dispatch(hideNotification(id))
}, 5000)
}
// component.js
showNotificationWithTimeout('You just logged in.')
// otherComponent.js
showNotificationWithTimeout('You just logged out.')
Parece mais simples, mas não recomendamos essa abordagem . A principal razão pela qual não gostamos é porque força a loja a ser um singleton . Isso dificulta muito a implementação renderização do servidor . No servidor, você deseja que cada solicitação tenha seu próprio armazenamento, para que usuários diferentes obtenham diferentes dados pré-carregados.
Uma loja singleton também torna os testes mais difíceis. Você não pode mais zombar de uma loja ao testar criadores de ação, porque eles fazem referência a uma loja real específica exportada de um módulo específico. Você não pode nem redefinir seu estado de fora.
Portanto, embora você possa exportar tecnicamente uma loja singleton de um módulo, nós a desencorajamos. Não faça isso, a menos que tenha certeza de que seu aplicativo nunca adicionará renderização de servidor.
Voltando à versão anterior:
// actions.js
// ...
let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Isso resolve os problemas com a duplicação da lógica e nos salva das condições da corrida.
Thunk Middleware
Para aplicativos simples, a abordagem deve ser suficiente. Não se preocupe com o middleware, se você estiver feliz com isso.
Em aplicativos maiores, no entanto, você pode encontrar alguns inconvenientes.
Por exemplo, parece lamentável que tenhamos que repassar dispatch
. Isso torna mais difícil separar componentes de contêiner e de apresentação, pois qualquer componente que despacha ações de Redux de forma assíncrona da maneira acima deve ser aceito dispatch
como um suporte para que possa passar adiante. Você não pode mais vincular os criadores de ação connect()
porqueshowNotificationWithTimeout()
não é realmente um criador de ação. Não retorna uma ação Redux.
Além disso, pode ser estranho lembrar quais funções são criadoras de ação síncronas showNotification()
e quais são ajudantes assíncronas, comoshowNotificationWithTimeout()
. Você precisa usá-los de maneira diferente e tome cuidado para não confundi-los.
Essa foi a motivação para encontrar uma maneira de "legitimar" esse padrão de fornecer dispatch
a uma função auxiliar e ajudar o Redux a "ver" tais criadores de ação assíncronos como um caso especial de criadores de ação normal, em vez de funções totalmente diferentes.
Se você ainda está conosco e também o reconhece como um problema no seu aplicativo, pode usar o middleware Redux Thunk .
Em essência, o Redux Thunk ensina o Redux a reconhecer tipos especiais de ações que de fato funcionam:
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(
reducer,
applyMiddleware(thunk)
)
// It still recognizes plain object actions
store.dispatch({ type: 'INCREMENT' })
// But with thunk middleware, it also recognizes functions
store.dispatch(function (dispatch) {
// ... which themselves may dispatch many times
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
setTimeout(() => {
// ... even asynchronously!
dispatch({ type: 'DECREMENT' })
}, 1000)
})
Quando esse middleware está ativado, se você despacha uma função , o middleware Redux Thunk fornecedispatch
como argumento. Ele também “engolirá” essas ações, portanto, não se preocupe com seus redutores recebendo argumentos de funções estranhos. Seus redutores receberão apenas ações simples de objetos - emitidas diretamente ou emitidas pelas funções, como acabamos de descrever.
Isso não parece muito útil, não é? Não nesta situação em particular. No entanto, permite-nos declarar showNotificationWithTimeout()
como um criador de ação Redux comum:
// actions.js
function showNotification(id, text) {
return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
return { type: 'HIDE_NOTIFICATION', id }
}
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
Observe como a função é quase idêntica à que escrevemos na seção anterior. No entanto, ele não aceita dispatch
como o primeiro argumento. Em vez disso, ele retorna uma função que aceita dispatch
como o primeiro argumento.
Como o usaríamos em nosso componente? Definitivamente, poderíamos escrever isso:
// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)
Estamos chamando o criador de ação assíncrona para obter a função interna que deseja apenas dispatch
e depois passamos dispatch
.
No entanto, isso é ainda mais complicado do que a versão original! Por que nós seguimos esse caminho?
Por causa do que eu te disse antes. Se o middleware Redux Thunk estiver ativado, sempre que você tentar despachar uma função em vez de um objeto de ação, o middleware chamará essa função com o dispatch
próprio método como o primeiro argumento .
Então, podemos fazer isso:
// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
Por fim, despachar uma ação assíncrona (na verdade, uma série de ações) não parece diferente de despachar uma única ação de forma síncrona para o componente. O que é bom porque os componentes não devem se importar se algo acontece de forma síncrona ou assíncrona. Nós apenas abstraímos isso.
Observe que, uma vez que "ensinamos" o Redux a reconhecer esses criadores de ação "especiais" (os chamamos de criadores de thunk action), agora podemos usá-los em qualquer lugar em que usaríamos criadores de ação regulares. Por exemplo, podemos usá-los com connect()
:
// actions.js
function showNotification(id, text) {
return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
return { type: 'HIDE_NOTIFICATION', id }
}
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
// component.js
import { connect } from 'react-redux'
// ...
this.props.showNotificationWithTimeout('You just logged in.')
// ...
export default connect(
mapStateToProps,
{ showNotificationWithTimeout }
)(MyComponent)
Estado da leitura em Thunks
Geralmente, seus redutores contêm a lógica de negócios para determinar o próximo estado. No entanto, os redutores somente entram em ação após o envio das ações. E se você tiver um efeito colateral (como chamar uma API) em um criador de ações de thunk e quiser evitá-lo sob alguma condição?
Sem usar o middleware thunk, basta fazer esta verificação dentro do componente:
// component.js
if (this.props.areNotificationsEnabled) {
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}
No entanto, o objetivo de extrair um criador de ações era centralizar essa lógica repetitiva em muitos componentes. Felizmente, o Redux Thunk oferece uma maneira de ler o estado atual da loja Redux. Além disso dispatch
, ele também passa getState
como o segundo argumento para a função que você retorna do seu criador de ações de thunk. Isso permite que o thunk leia o estado atual da loja.
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch, getState) {
// Unlike in a regular action creator, we can exit early in a thunk
// Redux doesn’t care about its return value (or lack of it)
if (!getState().areNotificationsEnabled) {
return
}
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
Não abuse deste padrão. É bom para resgatar chamadas de API quando há dados em cache disponíveis, mas não é uma base muito boa para construir a lógica de negócios. Se você usar getState()
apenas para despachar condicionalmente ações diferentes, considere colocar a lógica de negócios nos redutores.
Próximos passos
Agora que você tem uma intuição básica sobre como os thunks funcionam, confira o exemplo assíncrono do Redux que os utiliza.
Você pode encontrar muitos exemplos nos quais os thunks retornam promessas. Isso não é necessário, mas pode ser muito conveniente. O Redux não se importa com o que você retorna de um thunk, mas fornece seu valor de retorno dispatch()
. É por isso que você pode retornar uma promessa a partir de uma conversão e aguardar a conclusão, ligando dispatch(someThunkReturningPromise()).then(...)
.
Você também pode dividir criadores de ação thunk complexos em vários criadores de ação thunk menores. O dispatch
método fornecido pelos thunks pode aceitar os próprios thunks, para que você possa aplicar o padrão recursivamente. Novamente, isso funciona melhor com o Promises, porque você pode implementar o fluxo de controle assíncrono.
Para alguns aplicativos, você pode se deparar com uma situação em que seus requisitos de fluxo de controle assíncrono são muito complexos para serem expressos com thunks. Por exemplo, tentar novamente solicitações com falha, fluxo de nova autorização com tokens ou uma integração passo a passo pode ser muito detalhado e propenso a erros quando escrito dessa maneira. Nesse caso, convém procurar soluções de fluxo de controle assíncrono mais avançadas, como Redux Saga ou Redux Loop . Avalie-os, compare os exemplos relevantes para suas necessidades e escolha o que você mais gosta.
Finalmente, não use nada (incluindo thunks) se você não tiver a necessidade genuína deles. Lembre-se de que, dependendo dos requisitos, sua solução pode parecer tão simples quanto
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Não se preocupe, a menos que você saiba por que está fazendo isso.
redux-saga
resposta com base se você quiser algo melhor do que thunks. Resposta tardia, para que você tenha que rolar muito tempo antes de vê-la aparecer :) não significa que não vale a pena ler. Aqui está um atalho: stackoverflow.com/a/38574266/82609 #