Como despachar uma ação Redux com um tempo limite?

891

Eu tenho uma ação que atualiza o estado de notificação do meu aplicativo. Normalmente, esta notificação será um erro ou informação de algum tipo. Preciso despachar outra ação após 5 segundos que retornará o estado de notificação ao inicial, portanto, nenhuma notificação. A principal razão por trás disso é fornecer funcionalidade em que as notificações desaparecem automaticamente após 5 segundos.

Não tive sorte em usar setTimeoute retornar outra ação e não consigo descobrir como isso é feito online. Portanto, qualquer conselho é bem-vindo.

Ilja
fonte
30
Não se esqueça de verificar minha redux-sagaresposta 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 #
Sebastien Lorber
5
Sempre que você setTimeout não se esqueça de limpar o temporizador usando clearTimeout em componentWillUnMount método de ciclo de vida
Hemadri Dasari
2
redux-saga é legal, mas eles não parecem ter suporte para respostas digitadas das funções do gerador. Pode importar se você estiver usando texto datilografado com reagir.
Crhistian Ramirez

Respostas:

2617

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, dispatchmas, 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 dispatchdiretamente 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 dispatchcomo 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 dispatcha 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 dispatchcomo o primeiro argumento. Em vez disso, ele retorna uma função que aceita dispatchcomo 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 dispatche 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 dispatchpró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 getStatecomo 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 dispatchmé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.

Dan Abramov
fonte
27
Ações assíncronas parecem uma solução simples e elegante para um problema comum. Por que o suporte a eles não é reduxado sem a necessidade de middleware? Essa resposta poderia ser muito mais concisa.
Phil Mander
83
@PhilMander Porque existem muitos padrões alternativos, como github.com/raisemarketplace/redux-loop ou github.com/yelouafi/redux-saga, que são igualmente elegantes (se não mais). Redux é uma ferramenta de baixo nível. Você pode criar um superconjunto que desejar e distribuí-lo separadamente.
Dan Abramov 28/02
16
Você pode explicar o seguinte: * considere colocar a lógica de negócios nos redutores *, isso significa que eu devo despachar uma ação e, em seguida, determinar no redutor quais ações adicionais serão despachadas dependendo do meu estado? Minha pergunta é: então despacho outras ações diretamente no redutor e, se não, de onde as despacho?
Froginvasion 13/04/16
25
Esta frase se aplica apenas a maiúsculas e minúsculas. Por exemplo, se você escrever, if (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' })talvez deva apenas dispatch({ type: 'C', something: cond })optar por ignorar a ação nos redutores, dependendo action.somethingdo estado atual.
Dan Abramov
29
@DanAbramov Você recebeu meu voto positivo apenas por isso "A menos que você tenha esse problema, use o que a linguagem oferece e procure a solução mais simples." Só depois eu percebi quem o escreveu!
Matt Lacey
189

Usando Redux-saga

Como disse Dan Abramov, se você quiser um controle mais avançado sobre seu código assíncrono, poderá dar uma olhada na redux-saga .

Esta resposta é um exemplo simples. Se você deseja melhores explicações sobre por que o redux-saga pode ser útil para a sua aplicação, verifique esta outra resposta .

A idéia geral é que o Redux-saga ofereça um interpretador de geradores ES6 que permita que você escreva facilmente códigos assíncronos que se assemelhem a códigos síncronos (é por isso que você encontrará muitas vezes infinitas loops no Redux-saga). De alguma forma, o Redux-saga está construindo sua própria linguagem diretamente dentro do Javascript. O Redux-saga pode parecer um pouco difícil de aprender no começo, porque você precisa de conhecimentos básicos sobre geradores, mas também entende o idioma oferecido pelo Redux-saga.

Vou tentar aqui descrever aqui o sistema de notificação que construí em cima da redux-saga. Este exemplo atualmente é executado em produção.

Especificação avançada do sistema de notificação

  • Você pode solicitar que uma notificação seja exibida
  • Você pode solicitar uma notificação para ocultar
  • Uma notificação não deve ser exibida por mais de 4 segundos
  • Várias notificações podem ser exibidas ao mesmo tempo
  • Não podem ser exibidas mais de três notificações ao mesmo tempo
  • Se uma notificação for solicitada enquanto já houver 3 notificações exibidas, faça uma fila / adie-a.

Resultado

Captura de tela do meu aplicativo de produção Stample.co

Torradas

Código

Aqui eu chamei a notificação de toastmas este é um detalhe de nomeação.

function* toastSaga() {

    // Some config constants
    const MaxToasts = 3;
    const ToastDisplayTime = 4000;


    // Local generator state: you can put this state in Redux store
    // if it's really important to you, in my case it's not really
    let pendingToasts = []; // A queue of toasts waiting to be displayed
    let activeToasts = []; // Toasts currently displayed


    // Trigger the display of a toast for 4 seconds
    function* displayToast(toast) {
        if ( activeToasts.length >= MaxToasts ) {
            throw new Error("can't display more than " + MaxToasts + " at the same time");
        }
        activeToasts = [...activeToasts,toast]; // Add to active toasts
        yield put(events.toastDisplayed(toast)); // Display the toast (put means dispatch)
        yield call(delay,ToastDisplayTime); // Wait 4 seconds
        yield put(events.toastHidden(toast)); // Hide the toast
        activeToasts = _.without(activeToasts,toast); // Remove from active toasts
    }

    // Everytime we receive a toast display request, we put that request in the queue
    function* toastRequestsWatcher() {
        while ( true ) {
            // Take means the saga will block until TOAST_DISPLAY_REQUESTED action is dispatched
            const event = yield take(Names.TOAST_DISPLAY_REQUESTED);
            const newToast = event.data.toastData;
            pendingToasts = [...pendingToasts,newToast];
        }
    }


    // We try to read the queued toasts periodically and display a toast if it's a good time to do so...
    function* toastScheduler() {
        while ( true ) {
            const canDisplayToast = activeToasts.length < MaxToasts && pendingToasts.length > 0;
            if ( canDisplayToast ) {
                // We display the first pending toast of the queue
                const [firstToast,...remainingToasts] = pendingToasts;
                pendingToasts = remainingToasts;
                // Fork means we are creating a subprocess that will handle the display of a single toast
                yield fork(displayToast,firstToast);
                // Add little delay so that 2 concurrent toast requests aren't display at the same time
                yield call(delay,300);
            }
            else {
                yield call(delay,50);
            }
        }
    }

    // This toast saga is a composition of 2 smaller "sub-sagas" (we could also have used fork/spawn effects here, the difference is quite subtile: it depends if you want toastSaga to block)
    yield [
        call(toastRequestsWatcher),
        call(toastScheduler)
    ]
}

E o redutor:

const reducer = (state = [],event) => {
    switch (event.name) {
        case Names.TOAST_DISPLAYED:
            return [...state,event.data.toastData];
        case Names.TOAST_HIDDEN:
            return _.without(state,event.data.toastData);
        default:
            return state;
    }
};

Uso

Você pode simplesmente despachar TOAST_DISPLAY_REQUESTEDeventos. Se você enviar quatro solicitações, apenas três notificações serão exibidas e a quarta aparecerá um pouco mais tarde quando a 1ª notificação desaparecer.

Observe que eu não recomendo especificamente o envio TOAST_DISPLAY_REQUESTEDpelo JSX. Você prefere adicionar outra saga que ouça seus eventos de aplicativos já existentes e, em seguida, despache oTOAST_DISPLAY_REQUESTED : seu componente que aciona a notificação, não precisa estar fortemente acoplado ao sistema de notificação.

Conclusão

Meu código não é perfeito, mas é executado em produção com 0 bugs por meses. Redux-saga e geradores são um pouco difíceis inicialmente, mas depois que você os entende, esse tipo de sistema é muito fácil de construir.

É muito fácil implementar regras mais complexas, como:

  • quando muitas notificações estiverem "na fila", forneça menos tempo de exibição para cada notificação, para que o tamanho da fila possa diminuir mais rapidamente.
  • detectar alterações no tamanho da janela e alterar o número máximo de notificações exibidas de acordo (por exemplo, área de trabalho = 3, retrato telefônico = 2, paisagem do telefone = 1)

Honestamente, boa sorte em implementar esse tipo de coisa corretamente com thunks.

Note que você pode fazer exatamente o mesmo tipo de coisa com redux-observable, que é muito semelhante à redux-saga. É quase o mesmo e é uma questão de gosto entre geradores e RxJS.

Sebastien Lorber
fonte
18
Desejo que sua resposta tenha chegado mais cedo quando a pergunta foi feita, porque não posso concordar mais com o uso da biblioteca de efeitos colaterais Saga para lógica de negócios como esta. Redutores e criadores de ação são para transições de estado. Os fluxos de trabalho não são iguais às funções de transição de estado. Os fluxos de trabalho passam pelas transições, mas não são elas próprias. O Redux + React não possui isso por conta própria - é exatamente por isso que o Redux Saga é tão útil.
Atticus
4
Obrigado, tento fazer o possível para tornar popular o redux-saga por esses motivos :) poucas pessoas pensam que o redux-saga atualmente é apenas um substituto para os thunks e não vêem como o redux-saga permite fluxos de trabalho complexos e dissociados
Sebastien Lorber
1
Exatamente. Ações e redutores fazem parte da máquina de estado. Às vezes, para fluxos de trabalho complexos, você precisa de outra coisa para orquestrar a máquina de estado que não faz parte diretamente da própria máquina de estado!
Atticus
2
Ações: Cargas úteis / eventos para o estado de transição. Redutores: funções de transição de estado. Componentes: interfaces de usuário que refletem o estado. Mas falta uma peça importante - como você gerencia o processo de muitas transições que possuem uma lógica própria para determinar qual transição será executada a seguir? Redux Saga!
Atticus
2
@mrbrdo se você ler atentamente a minha resposta você vai notar que os tempos de espera de notificação são realmente tratado com yield call(delay,timeoutValue);: não é a mesma API, mas tem o mesmo efeito
Sebastien Lorber
25

Um repositório com projetos de amostra

Atualmente, existem quatro projetos de amostra:

  1. Escrevendo código assíncrono em linha
  2. Extraindo criador de ação assíncrona
  3. Usar Redux Thunk
  4. Use Redux Saga

A resposta aceita é incrível.

Mas há algo faltando:

  1. Nenhum projeto de amostra executável, apenas alguns trechos de código.
  2. Nenhum código de amostra para outras alternativas, como:
    1. Redux Saga

Então, eu criei o repositório Hello Async para adicionar as coisas ausentes:

  1. Projetos executáveis. Você pode fazer o download e executá-los sem modificação.
  2. Forneça código de exemplo para mais alternativas:

Redux Saga

A resposta aceita já fornece trechos de código de amostra para Código assíncrono em linha, Gerador de ação assíncrona e Redux Thunk. Por uma questão de integridade, forneço trechos de código para o Redux Saga:

// actions.js

export const showNotification = (id, text) => {
  return { type: 'SHOW_NOTIFICATION', id, text }
}

export const hideNotification = (id) => {
  return { type: 'HIDE_NOTIFICATION', id }
}

export const showNotificationWithTimeout = (text) => {
  return { type: 'SHOW_NOTIFICATION_WITH_TIMEOUT', text }
}

Ações são simples e puras.

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

Nada é especial com o componente.

// sagas.js

import { takeEvery, delay } from 'redux-saga'
import { put } from 'redux-saga/effects'
import { showNotification, hideNotification } from './actions'

// Worker saga
let nextNotificationId = 0
function* showNotificationWithTimeout (action) {
  const id = nextNotificationId++
  yield put(showNotification(id, action.text))
  yield delay(5000)
  yield put(hideNotification(id))
}

// Watcher saga, will invoke worker saga above upon action 'SHOW_NOTIFICATION_WITH_TIMEOUT'
function* notificationSaga () {
  yield takeEvery('SHOW_NOTIFICATION_WITH_TIMEOUT', showNotificationWithTimeout)
}

export default notificationSaga

Sagas são baseadas em geradores ES6

// index.js

import createSagaMiddleware from 'redux-saga'
import saga from './sagas'

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

sagaMiddleware.run(saga)

Comparado ao Redux Thunk

Prós

  • Você não acaba no inferno de retorno de chamada.
  • Você pode testar seus fluxos assíncronos facilmente.
  • Suas ações permanecem puras.

Contras

  • Depende dos ES6 Generators, que são relativamente novos.

Consulte o projeto executável se os trechos de código acima não responderem a todas as suas perguntas.

Tyler Long
fonte
23

Você pode fazer isso com redux-thunk . Há um guia no documento redux para ações assíncronas como setTimeout.

Fatih Erikli
fonte
Apenas uma pergunta rápida de acompanhamento, ao usar o middleware, applyMiddleware(ReduxPromise, thunk)(createStore)é assim que você adiciona vários middleware (coma separados?), Pois eu não consigo fazer o thunk funcionar.
Ilja 15/02
1
@Ilja Isso deve funcionar:const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
geniuscarrier
22

Eu recomendaria também dar uma olhada no padrão SAM .

O padrão SAM defende a inclusão de um "predicado da próxima ação", em que ações (automáticas) como "as notificações desaparecem automaticamente após 5 segundos" são acionadas após a atualização do modelo (modelo do SAM ~ estado redutor + armazenamento).

O padrão defende ações de seqüenciamento e mutações de modelo uma por vez, porque o "estado de controle" do modelo "controla" quais ações são ativadas e / ou executadas automaticamente pelo predicado da próxima ação. Você simplesmente não pode prever (em geral) qual o estado do sistema antes do processamento de uma ação e, portanto, se sua próxima ação esperada será permitida / possível.

Por exemplo, o código,

export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

não seria permitido com o SAM, porque o fato de uma ação hideNotification poder ser despachada depende do modelo que aceita com êxito o valor "showNotication: true". Pode haver outras partes do modelo que o impedem de aceitá-lo e, portanto, não há razão para acionar a ação hideNotification.

Eu recomendo que implemente um predicado de próxima ação adequado após a atualização das lojas e o novo estado de controle do modelo. Essa é a maneira mais segura de implementar o comportamento que você está procurando.

Você pode se juntar a nós no Gitter, se quiser. Há também um guia de introdução ao SAM disponível aqui .

Jean-Jacques Dubray
fonte
Eu apenas arranhei a superfície até agora, mas já estou emocionada com o padrão SAM. V = S( vm( M.present( A(data) ) ), nap(M))é simplesmente lindo. Obrigado por compartilhar seus pensamentos e experiências. Eu vou cavar mais fundo.
@ Ftor, obrigado! quando escrevi pela primeira vez, tive o mesmo sentimento. Eu uso o SAM na produção há quase um ano e não consigo pensar em um momento em que senti que precisava de uma biblioteca para implementar o SAM (mesmo que pareça, quando posso ver quando ele pode ser usado). Apenas uma linha de código, é isso! O SAM produz código isomórfico, não há ambiguidade em como lidar com chamadas assíncronas ... Não consigo pensar em uma época em que eu, o que estou fazendo?
Metaprogrammer>
O SAM é um verdadeiro padrão de engenharia de software (acabou de produzir um Alexa SDK com ele). Ele é baseado no TLA + e tenta trazer o poder desse trabalho incrível para todos os desenvolvedores. O SAM corrige três aproximações que (praticamente) todo mundo usa há décadas: - ações podem manipular o estado do aplicativo - atribuições equivalem a mutações - não há definição precisa do que é uma etapa de programação (por exemplo, é uma etapa = b * ca , são 1 / leia b, c 2 / calcule b * c, 3 / atribua a com o resultado três etapas diferentes?
metaprogrammer
20

Depois de experimentar as várias abordagens populares (criadores de ação, thunks, sagas, épicos, efeitos, middleware personalizado), eu ainda sentia que talvez houvesse espaço para melhorias, então documentei minha jornada neste artigo do blog, Onde coloco minha lógica de negócios? um aplicativo React / Redux?

Assim como as discussões aqui, tentei contrastar e comparar as várias abordagens. Eventualmente, isso me levou a apresentar uma nova biblioteca redux-logic, que se inspira em épicos, sagas e middleware personalizado.

Ele permite interceptar ações para validar, verificar, autorizar e fornecer uma maneira de executar E / S assíncronas.

Algumas funcionalidades comuns podem ser simplesmente declaradas como devolução, limitação, cancelamento e apenas o uso da resposta da solicitação mais recente (takeLatest). A redux-logic envolve seu código, fornecendo essa funcionalidade para você.

Isso libera você para implementar sua lógica de negócios principal da maneira que desejar. Você não precisa usar observáveis ​​ou geradores, a menos que queira. Use funções e retornos de chamada, promessas, funções assíncronas (async / waitit), etc.

O código para fazer uma notificação 5s simples seria algo como:

const notificationHide = createLogic({
  // the action type that will trigger this logic
  type: 'NOTIFICATION_DISPLAY',
  
  // your business logic can be applied in several
  // execution hooks: validate, transform, process
  // We are defining our code in the process hook below
  // so it runs after the action hit reducers, hide 5s later
  process({ getState, action }, dispatch) {
    setTimeout(() => {
      dispatch({ type: 'NOTIFICATION_CLEAR' });
    }, 5000);
  }
});
    

Eu tenho um exemplo de notificação mais avançado no meu repositório que funciona de maneira semelhante ao que Sebastian Lorber descreveu onde você pode limitar a exibição a N itens e alternar entre os que estão na fila. exemplo de notificação redux-logic

Eu tenho uma variedade de exemplos ao vivo do redux-logic jsfiddle, bem como exemplos completos . Continuo trabalhando em documentos e exemplos.

Eu adoraria ouvir seus comentários.

Jeff Barczewski
fonte
Não tenho certeza se gosto da sua biblioteca, mas gosto do seu artigo! Muito bem, cara! Você já trabalhou bastante para economizar o tempo dos outros.
Tyler Longo
2
Criei um exemplo de projeto para redux-logic aqui: github.com/tylerlong/hello-async/tree/master/redux-logic Acho que é um software de design bem projetado e não vejo grandes desvantagens em comparação com outros alternativas.
Tyler Longo
9

Entendo que essa pergunta é um pouco antiga, mas vou apresentar outra solução usando o redux-observable aka. Épico.

Citando a documentação oficial:

O que é observável no redux?

Middleware baseado em RxJS 5 para Redux. Componha e cancele ações assíncronas para criar efeitos colaterais e muito mais.

Um épico é o primitivo central do redux observável.

É uma função que executa um fluxo de ações e retorna um fluxo de ações. Ações dentro, ações fora.

Em mais ou menos palavras, você pode criar uma função que receba ações por meio de um fluxo e, em seguida, retornar um novo fluxo de ações (usando efeitos colaterais comuns, como tempos limite, atrasos, intervalos e solicitações).

Deixe-me postar o código e depois explicar um pouco mais sobre ele

store.js

import {createStore, applyMiddleware} from 'redux'
import {createEpicMiddleware} from 'redux-observable'
import {Observable} from 'rxjs'
const NEW_NOTIFICATION = 'NEW_NOTIFICATION'
const QUIT_NOTIFICATION = 'QUIT_NOTIFICATION'
const NOTIFICATION_TIMEOUT = 2000

const initialState = ''
const rootReducer = (state = initialState, action) => {
  const {type, message} = action
  console.log(type)
  switch(type) {
    case NEW_NOTIFICATION:
      return message
    break
    case QUIT_NOTIFICATION:
      return initialState
    break
  }

  return state
}

const rootEpic = (action$) => {
  const incoming = action$.ofType(NEW_NOTIFICATION)
  const outgoing = incoming.switchMap((action) => {
    return Observable.of(quitNotification())
      .delay(NOTIFICATION_TIMEOUT)
      //.takeUntil(action$.ofType(NEW_NOTIFICATION))
  });

  return outgoing;
}

export function newNotification(message) {
  return ({type: NEW_NOTIFICATION, message})
}
export function quitNotification(message) {
  return ({type: QUIT_NOTIFICATION, message});
}

export const configureStore = () => createStore(
  rootReducer,
  applyMiddleware(createEpicMiddleware(rootEpic))
)

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {configureStore} from './store.js'
import {Provider} from 'react-redux'

const store = configureStore()

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

App.js

import React, { Component } from 'react';
import {connect} from 'react-redux'
import {newNotification} from './store.js'

class App extends Component {

  render() {
    return (
      <div className="App">
        {this.props.notificationExistance ? (<p>{this.props.notificationMessage}</p>) : ''}
        <button onClick={this.props.onNotificationRequest}>Click!</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    notificationExistance : state.length > 0,
    notificationMessage : state
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onNotificationRequest: () => dispatch(newNotification(new Date().toDateString()))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

O código-chave para resolver esse problema é tão fácil quanto você pode ver, a única coisa que parece diferente das outras respostas é a função rootEpic.

Ponto 1. Assim como as sagas, você precisa combinar os épicos para obter uma função de nível superior que receba um fluxo de ações e retorne um fluxo de ações, para poder usá-lo com o createEpicMiddleware da fábrica de middleware . No nosso caso, precisamos apenas de um, portanto, temos apenas o nosso rootEpic, para que não tenhamos que combinar nada, mas é um fato bom saber.

Ponto 2. Nosso rootEpic, que cuida da lógica dos efeitos colaterais, leva apenas 5 linhas de código, o que é incrível! Incluindo o fato de que é praticamente declarativo!

Ponto 3. Explicação rootEpic linha a linha (nos comentários)

const rootEpic = (action$) => {
  // sets the incoming constant as a stream 
  // of actions with  type NEW_NOTIFICATION
  const incoming = action$.ofType(NEW_NOTIFICATION)
  // Merges the "incoming" stream with the stream resulting for each call
  // This functionality is similar to flatMap (or Promise.all in some way)
  // It creates a new stream with the values of incoming and 
  // the resulting values of the stream generated by the function passed
  // but it stops the merge when incoming gets a new value SO!,
  // in result: no quitNotification action is set in the resulting stream
  // in case there is a new alert
  const outgoing = incoming.switchMap((action) => {
    // creates of observable with the value passed 
    // (a stream with only one node)
    return Observable.of(quitNotification())
      // it waits before sending the nodes 
      // from the Observable.of(...) statement
      .delay(NOTIFICATION_TIMEOUT)
  });
  // we return the resulting stream
  return outgoing;
}

Espero que ajude!

cnexans
fonte
Você poderia explicar o que os métodos específicos da API estão fazendo aqui, como switchMap?
Dmitri Zaitsev
1
Estamos usando redux-observable em nosso aplicativo React Native no Windows. É uma solução de implementação elegante para um problema complexo e altamente assíncrono e possui suporte fantástico por meio dos problemas de canal Gitter e GitHub. A camada extra de complexidade só vale a pena se você chegar ao problema exato que ele pretende resolver, é claro.
Matt Hargett
8

Por que deveria ser tão difícil? É apenas lógica da interface do usuário. Use uma ação dedicada para definir dados de notificação:

dispatch({ notificationData: { message: 'message', expire: +new Date() + 5*1000 } })

e um componente dedicado para exibi-lo:

const Notifications = ({ notificationData }) => {
    if(notificationData.expire > this.state.currentTime) {
      return <div>{notificationData.message}</div>
    } else return null;
}

Nesse caso, as perguntas devem ser "como você limpa o estado antigo?", "Como notificar um componente que o tempo mudou"

Você pode implementar alguma ação TIMEOUT que é despachada em setTimeout a partir de um componente.

Talvez seja bom limpá-lo sempre que uma nova notificação for exibida.

Enfim, deve haver algum setTimeoutlugar, certo? Por que não fazer isso em um componente

setTimeout(() => this.setState({ currentTime: +new Date()}), 
           this.props.notificationData.expire-(+new Date()) )

A motivação é que a funcionalidade "notificação desaparece" é realmente uma preocupação da interface do usuário. Por isso, simplifica os testes para sua lógica de negócios.

Não parece fazer sentido testar como é implementado. Faz sentido verificar quando a notificação deve expirar. Assim, menos código para stub, testes mais rápidos, código mais limpo.

Vanuan
fonte
1
Essa deve ser a resposta principal.
mmla
6

Se você deseja controlar o tempo limite de ações seletivas, tente a abordagem do middleware . Eu enfrentei um problema semelhante ao lidar com ações baseadas em promessas seletivamente e essa solução era mais flexível.

Digamos que seu criador de ações seja assim:

//action creator
buildAction = (actionData) => ({
    ...actionData,
    timeout: 500
})

o tempo limite pode conter vários valores na ação acima

  • número em ms - por um período de tempo limite específico
  • true - por um tempo limite constante. (tratado no middleware)
  • indefinido - para envio imediato

Sua implementação de middleware ficaria assim:

//timeoutMiddleware.js
const timeoutMiddleware = store => next => action => {

  //If your action doesn't have any timeout attribute, fallback to the default handler
  if(!action.timeout) {
    return next (action)
  }

  const defaultTimeoutDuration = 1000;
  const timeoutDuration = Number.isInteger(action.timeout) ? action.timeout || defaultTimeoutDuration;

//timeout here is called based on the duration defined in the action.
  setTimeout(() => {
    next (action)
  }, timeoutDuration)
}

Agora você pode rotear todas as suas ações por essa camada de middleware usando o redux.

createStore(reducer, applyMiddleware(timeoutMiddleware))

Você pode encontrar alguns exemplos semelhantes aqui

Yash
fonte
5

A maneira apropriada de fazer isso é usando o Redux Thunk, que é um middleware popular para o Redux, conforme a documentação do Redux Thunk:

"O middleware Redux Thunk permite gravar criadores de ação que retornam uma função em vez de uma ação. O thunk pode ser usado para atrasar o despacho de uma ação ou para despachar apenas se uma determinada condição for atendida. A função interna recebe os métodos de armazenamento despachar e getState como parâmetros ".

Então, basicamente, ele retorna uma função e você pode atrasar seu envio ou colocá-lo em um estado de condição.

Então, algo assim fará o trabalho para você:

import ReduxThunk from 'redux-thunk';

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment());
    }, 5000);
  };
}
Alireza
fonte
4

É simples Use o pacote trim-redux e escreva assim componentDidMountou em outro lugar e mate-o componentWillUnmount.

componentDidMount() {
  this.tm = setTimeout(function() {
    setStore({ age: 20 });
  }, 3000);
}

componentWillUnmount() {
  clearTimeout(this.tm);
}
Mohmmad Ebrahimi Aval
fonte
3

O Redux em si é uma biblioteca bastante detalhada e, para essas coisas, você teria que usar algo como Redux-thunk , que dará uma dispatchfunção, para que você possa despachar o fechamento da notificação após alguns segundos.

Eu criei uma biblioteca para resolver problemas como verbosidade e composição, e seu exemplo será parecido com o seguinte:

import { createTile, createSyncTile } from 'redux-tiles';
import { sleep } from 'delounce';

const notifications = createSyncTile({
  type: ['ui', 'notifications'],
  fn: ({ params }) => params.data,
  // to have only one tile for all notifications
  nesting: ({ type }) => [type],
});

const notificationsManager = createTile({
  type: ['ui', 'notificationManager'],
  fn: ({ params, dispatch, actions }) => {
    dispatch(actions.ui.notifications({ type: params.type, data: params.data }));
    await sleep(params.timeout || 5000);
    dispatch(actions.ui.notifications({ type: params.type, data: null }));
    return { closed: true };
  },
  nesting: ({ type }) => [type],
});

Assim, compomos ações de sincronização para mostrar notificações dentro da ação assíncrona, que podem solicitar algumas informações em segundo plano ou verificar mais tarde se a notificação foi fechada manualmente.

Bloomca
fonte