Come inviare un'azione Redux con un timeout?


891

Ho un'azione che aggiorna lo stato di notifica della mia domanda. Di solito, questa notifica sarà un errore o informazioni di qualche tipo. Devo quindi inviare un'altra azione dopo 5 secondi che restituirà lo stato di notifica a quello iniziale, quindi nessuna notifica. Il motivo principale alla base di ciò è fornire funzionalità in cui le notifiche scompaiono automaticamente dopo 5 secondi.

Non ho avuto fortuna con l'utilizzo setTimeoute la restituzione di un'altra azione e non riesco a trovare come farlo online. Quindi ogni consiglio è il benvenuto.


30
Non dimenticare di controllare la mia redux-sagarisposta di base se vuoi qualcosa di meglio dei thunk. Risposta tardiva, quindi devi scorrere a lungo prima di vederlo apparire :) non significa che non vale la pena leggere. Ecco una scorciatoia: stackoverflow.com/a/38574266/82609
Sebastien Lorber

5
Ogni volta che
imposti Timeout

2
redux-saga è interessante, ma non sembrano avere supporto per le risposte tipizzate dalle funzioni del generatore. Potrebbe essere importante se stai usando dattiloscritto con reagire.
Crhistian Ramirez,

Risposte:


2617

Non cadere nella trappola di pensare che una biblioteca dovrebbe prescrivere come fare tutto . Se vuoi fare qualcosa con un timeout in JavaScript, devi usare setTimeout. Non vi è alcun motivo per cui le azioni Redux dovrebbero essere diverse.

Redux non offrono alcuni modi alternativi di trattare con roba asincrona, ma si dovrebbe utilizzare solo quelli in cui ci si rende conto che si sta ripetendo troppo codice. A meno che tu non abbia questo problema, usa ciò che la lingua offre e scegli la soluzione più semplice.

Scrittura del codice asincrono in linea

Questo è di gran lunga il modo più semplice. E non c'è niente di specifico in Redux qui.

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

Allo stesso modo, dall'interno di un componente collegato:

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

L'unica differenza è che in un componente connesso di solito non hai accesso al negozio stesso, ma ricevi uno dispatch()o uno specifico creatore di azioni iniettato come oggetti di scena. Tuttavia questo non fa alcuna differenza per noi.

Se non ti piace fare errori di battitura quando invii le stesse azioni da componenti diversi, potresti voler estrarre i creatori di azioni invece di inviare oggetti di azione in linea:

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

Oppure, se li hai precedentemente associati con connect():

this.props.showNotification('You just logged in.')
setTimeout(() => {
  this.props.hideNotification()
}, 5000)

Finora non abbiamo utilizzato alcun middleware o altri concetti avanzati.

Estrarre Async Action Creator

L'approccio sopra funziona bene in casi semplici, ma potresti riscontrare alcuni problemi:

  • Ti costringe a duplicare questa logica ovunque tu voglia mostrare una notifica.
  • Le notifiche non hanno ID, quindi avrai una condizione di gara se mostri due notifiche abbastanza velocemente. Al termine del primo timeout, verrà inviato HIDE_NOTIFICATION, nascondendo erroneamente la seconda notifica prima che dopo il timeout.

Per risolvere questi problemi, è necessario estrarre una funzione che centralizza la logica di timeout e invia queste due azioni. Potrebbe apparire così:

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

Ora i componenti possono utilizzare showNotificationWithTimeoutsenza duplicare questa logica o avere condizioni di competizione con diverse notifiche:

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

Perché showNotificationWithTimeout()accetta dispatchcome primo argomento? Perché deve inviare le azioni al negozio. Normalmente un componente ha accesso, dispatchma poiché vogliamo che una funzione esterna prenda il controllo del dispacciamento, dobbiamo dargli il controllo del dispacciamento.

Se tu avessi un negozio singleton esportato da qualche modulo, potresti semplicemente importarlo e dispatchdirettamente su di esso:

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

Sembra più semplice ma non consigliamo questo approccio . Il motivo principale per cui non ci piace è perché impone al negozio di essere un singleton . Ciò rende molto difficile implementare il rendering del server . Sul server, si desidera che ogni richiesta abbia un proprio archivio, in modo che diversi utenti ottengano dati precaricati diversi.

Un negozio singleton rende anche i test più difficili. Non è più possibile deridere un negozio durante il test dei creatori di azioni perché fanno riferimento a un negozio reale specifico esportato da un modulo specifico. Non è nemmeno possibile ripristinare il suo stato dall'esterno.

Quindi, mentre tecnicamente puoi esportare un negozio singleton da un modulo, lo scoraggiamo. Non farlo se non sei sicuro che la tua app non aggiungerà mai il rendering del server.

Tornare alla versione precedente:

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

Questo risolve i problemi con la duplicazione della logica e ci salva dalle condizioni di gara.

Middleware Thunk

Per le app semplici, l'approccio dovrebbe essere sufficiente. Non preoccuparti del middleware se ne sei soddisfatto.

Nelle app più grandi, tuttavia, potresti riscontrare alcuni inconvenienti al suo interno.

Ad esempio, sembra sfortunato che dobbiamo passare in dispatchgiro. Ciò rende più complicato separare i contenitori e i componenti di presentazione poiché qualsiasi componente che invia le azioni Redux in modo asincrono nel modo sopra deve accettare dispatchcome prop per poterlo oltrepassare. Non puoi più solo legare i creatori di azioni connect()perché showNotificationWithTimeout()non è davvero un creatore di azioni. Non restituisce un'azione Redux.

Inoltre, può essere scomodo ricordare quali funzioni sono simili ai creatori di azioni sincrone showNotification()e quali sono gli helper asincroni showNotificationWithTimeout(). Devi usarli in modo diverso e fare attenzione a non confonderli tra loro.

Questa era la motivazione per trovare un modo per "legittimare" questo schema di fornitura dispatcha una funzione di supporto e aiutare Redux a "vedere" tali creatori di azioni asincrone come un caso speciale di normali creatori di azioni piuttosto che funzioni totalmente diverse.

Se sei ancora con noi e riconosci anche come un problema nella tua app, puoi usare il middleware Redux Thunk .

In breve, Redux Thunk insegna a Redux a riconoscere tipi speciali di azioni che sono in realtà funzioni:

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 questo middleware è abilitato, se si invia una funzione , il middleware Redux Thunk lo fornirà dispatchcome argomento. Inoltre "inghiottirà" tali azioni, quindi non preoccuparti che i tuoi riduttori ricevano argomenti di funzioni strane. I vostri riduttori riceveranno solo azioni di oggetti semplici, emesse direttamente o emesse dalle funzioni appena descritte.

Questo non sembra molto utile, vero? Non in questa situazione particolare. Tuttavia, ci consente di dichiarare showNotificationWithTimeout()come un normale creatore di azioni Redux:

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

Nota come la funzione è quasi identica a quella che abbiamo scritto nella sezione precedente. Tuttavia, non accetta dispatchcome primo argomento. Invece restituisce una funzione che accetta dispatchcome primo argomento.

Come lo utilizzeremmo nel nostro componente? Sicuramente, potremmo scrivere questo:

// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)

Stiamo chiamando il creatore dell'azione asincrona per ottenere la funzione interiore che vuole solo dispatch, e poi passiamo dispatch.

Tuttavia questo è ancora più imbarazzante della versione originale! Perché siamo andati anche così?

Per quello che ti ho detto prima. Se il middleware Redux Thunk è abilitato, ogni volta che si tenta di inviare una funzione anziché un oggetto azione, il middleware chiamerà quella funzione con il dispatchmetodo stesso come primo argomento .

Quindi possiamo farlo invece:

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))

Infine, l'invio di un'azione asincrona (in realtà, una serie di azioni) non sembra diverso dall'invio di una singola azione in modo sincrono al componente. Il che è positivo perché i componenti non dovrebbero preoccuparsi se qualcosa accade in modo sincrono o asincrono. L'abbiamo sottratto.

Si noti che poiché abbiamo "insegnato" a Redux di riconoscere tali creatori di azioni "speciali" (li chiamiamo creatori di azioni thunk ), ora possiamo usarli in qualsiasi luogo in cui utilizzeremmo creatori di azioni regolari. Ad esempio, possiamo usarli con 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)

Reading State in Thunks

Di solito i riduttori contengono la logica aziendale per determinare lo stato successivo. Tuttavia, i riduttori entrano in azione solo dopo l'invio delle azioni. Che cosa succede se si ha un effetto collaterale (come la chiamata di un'API) in un creatore di azioni thunk e si desidera prevenirlo in determinate condizioni?

Senza utilizzare il middleware thunk, esegui questo controllo all'interno del componente:

// component.js
if (this.props.areNotificationsEnabled) {
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}

Tuttavia, il punto di estrarre un creatore di azioni era centralizzare questa logica ripetitiva attraverso molti componenti. Fortunatamente, Redux Thunk ti offre un modo per leggere lo stato attuale del negozio Redux. Inoltre dispatch, passa anche getStatecome secondo argomento alla funzione che ritorni dal tuo creatore di azioni thunk. Ciò consente al thunk di leggere lo stato corrente del negozio.

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

Non abusare di questo schema. È utile per salvare le chiamate API quando sono disponibili dati memorizzati nella cache, ma non è un'ottima base su cui basare la propria logica aziendale. Se si utilizza getState()solo per inviare in modo condizionale azioni diverse, considerare invece di inserire la logica aziendale nei riduttori.

Prossimi passi

Ora che hai un'intuizione di base su come funzionano i thunk, controlla l' esempio asincrono di Redux che li utilizza.

Puoi trovare molti esempi in cui i thunk restituiscono Promesse. Questo non è necessario ma può essere molto conveniente. A Redux non importa cosa ritorni da un thunk, ma ti dà il suo valore di ritorno da dispatch(). Questo è il motivo per cui puoi restituire una Promessa da un thunk e attendere che venga completata chiamando dispatch(someThunkReturningPromise()).then(...).

Puoi anche dividere complessi creatori di azioni thunk in più piccoli creatori di azioni thunk. Il dispatchmetodo fornito dai thunk può accettare i thunk stessi, quindi puoi applicare il pattern in modo ricorsivo. Ancora una volta, questo funziona meglio con Promises perché puoi implementare un flusso di controllo asincrono.

Per alcune app, potresti trovarti in una situazione in cui i requisiti del flusso di controllo asincrono sono troppo complessi per essere espressi con i thunk. Ad esempio, riprovare richieste non riuscite, flusso di riautorizzazione con token o onboarding passo-passo può essere troppo dettagliato e soggetto a errori se scritto in questo modo. In questo caso, potresti voler esaminare soluzioni di flusso di controllo asincrono più avanzate come Redux Saga o Redux Loop . Valutali, confronta gli esempi pertinenti alle tue esigenze e scegli quello che ti piace di più.

Infine, non usare nulla (compresi i thunk) se non ne hai il bisogno reale. Ricorda che, a seconda dei requisiti, la tua soluzione potrebbe apparire semplice come

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

Non sudare se non sai perché lo stai facendo.


27
Le azioni asincrone sembrano una soluzione così semplice ed elegante a un problema comune. Perché il supporto per loro non è integrato in Redux senza la necessità di middleware? Questa risposta potrebbe quindi essere molto più concisa.
Phil Mander,

83
@PhilMander Perché ci sono molti modelli alternativi come github.com/raisemarketplace/redux-loop o github.com/yelouafi/redux-saga che sono altrettanto (se non più) eleganti. Redux è uno strumento di basso livello. Puoi creare un superset che ti piace e distribuirlo separatamente.
Dan Abramov,

16
Puoi spiegarlo: * potresti mettere la logica di business nei riduttori *, significa che dovrei inviare un'azione e quindi determinare nel riduttore quali ulteriori azioni inviare a seconda del mio stato? La mia domanda è: devo quindi inviare altre azioni direttamente nel mio riduttore e, in caso contrario, da dove le spedisco?
froginvasion,

25
Questa frase si applica solo al caso sincrono. Ad esempio, se scrivi, if (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' })forse dovresti semplicemente dispatch({ type: 'C', something: cond })e scegliere di ignorare l'azione nei riduttori invece a seconda action.somethingdello stato corrente.
Dan Abramov,

29
@DanAbramov Hai ottenuto il mio voto solo per questo "A meno che tu non abbia questo problema, usa ciò che la lingua offre e scegli la soluzione più semplice." Solo dopo mi sono reso conto di chi l'ha scritto!
Matt Lacey,

189

Usando Redux-saga

Come ha detto Dan Abramov, se vuoi un controllo più avanzato sul tuo codice asincrono, potresti dare un'occhiata a redux-saga .

Questa risposta è un semplice esempio, se vuoi spiegazioni migliori sul perché redux-saga possa essere utile per la tua applicazione, controlla questa altra risposta .

L'idea generale è che Redux-saga offre un interprete di generatori ES6 che ti permette di scrivere facilmente un codice asincrono che assomiglia a un codice sincrono (ecco perché spesso troverai loop infiniti in Redux-saga). In qualche modo, Redux-saga sta costruendo la propria lingua direttamente all'interno di Javascript. La saga di Redux può sembrare inizialmente un po 'difficile da imparare, perché hai bisogno di una conoscenza di base dei generatori, ma anche di capire il linguaggio offerto dalla saga di Redux.

Proverò qui a descrivere qui il sistema di notifica che ho costruito in cima a Redux-Saga. Questo esempio è attualmente in produzione.

Specifiche avanzate del sistema di notifica

  • È possibile richiedere la visualizzazione di una notifica
  • Puoi richiedere una notifica da nascondere
  • Una notifica non deve essere visualizzata per più di 4 secondi
  • È possibile visualizzare più notifiche contemporaneamente
  • Non è possibile visualizzare più di 3 notifiche contemporaneamente
  • Se viene richiesta una notifica mentre ci sono già 3 notifiche visualizzate, quindi accodare / posticipare.

Risultato

Schermata della mia app di produzione Stample.co

brindisi

Codice

Qui ho chiamato la notifica a toastma questo è un dettaglio di denominazione.

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 il riduttore:

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

Puoi semplicemente inviare TOAST_DISPLAY_REQUESTEDeventi. Se invii 4 richieste, verranno visualizzate solo 3 notifiche e la quarta apparirà un po 'più tardi una volta scomparsa la prima notifica.

Si noti che non raccomando specificamente l'invio TOAST_DISPLAY_REQUESTEDda JSX. Preferiresti aggiungere un'altra saga che ascolta i tuoi eventi di app già esistenti e quindi inviare TOAST_DISPLAY_REQUESTED: il tuo componente che attiva la notifica, non deve essere strettamente accoppiato al sistema di notifica.

Conclusione

Il mio codice non è perfetto ma funziona in produzione con 0 bug per mesi. Redux-saga e generatori sono inizialmente un po 'difficili, ma una volta che li capisci questo tipo di sistema è abbastanza facile da costruire.

È anche abbastanza facile implementare regole più complesse, come:

  • quando troppe notifiche vengono "messe in coda", fornire meno tempo di visualizzazione per ciascuna notifica in modo che le dimensioni della coda possano diminuire più rapidamente.
  • rilevare le modifiche alle dimensioni della finestra e modificare di conseguenza il numero massimo di notifiche visualizzate (ad esempio desktop = 3, ritratto telefono = 2, panorama telefono = 1)

Sinceramente, buona fortuna implementando questo tipo di cose correttamente con i thunk.

Nota che puoi fare esattamente lo stesso tipo di cose con l' osservazione redux che è molto simile alla redux-saga. È quasi lo stesso ed è una questione di gusti tra generatori e RxJS.


18
Vorrei che la tua risposta fosse arrivata prima quando veniva posta la domanda, perché non posso essere più d'accordo con l'uso della libreria di effetti collaterali di Saga per una logica aziendale come questa. Riduttori e creatori di azioni sono per le transizioni di stato. I flussi di lavoro non sono gli stessi delle funzioni di transizione dello stato. I flussi di lavoro attraversano le transizioni, ma non sono transizioni stesse. A Redux + React manca questo da soli - questo è esattamente il motivo per cui Redux Saga è così utile.
Atticus,

4
Grazie, cerco di fare del mio meglio per rendere popolare la redux-saga per questi motivi :) troppe persone pensano che attualmente la redux-saga sia solo una sostituzione dei thunk e non vedono come redux-saga consenta flussi di lavoro complessi e disaccoppiati
Sebastien Lorber

1
Esattamente. Azioni e riduttori fanno tutti parte della macchina a stati. A volte, per flussi di lavoro complessi, è necessario qualcos'altro per orchestrare la macchina a stati che non fa direttamente parte della macchina a stati stessa!
Atticus,

2
Azioni: payload / eventi allo stato di transizione. Riduttori: funzioni di transizione di stato. Componenti: interfacce utente che riflettono lo stato. Ma manca un pezzo importante: come gestisci il processo di molte transizioni che hanno tutte una propria logica che determina quale transizione eseguire successivamente? Redux Saga!
Atticus,

2
@mrbrdo se leggi attentamente la mia risposta noterai che i timeout di notifica sono effettivamente gestiti yield call(delay,timeoutValue);: non è la stessa API ma ha lo stesso effetto
Sebastien Lorber

25

Un repository con progetti di esempio

Attualmente ci sono quattro progetti di esempio:

  1. Scrittura del codice asincrono in linea
  2. Estrarre Async Action Creator
  3. Usa Redux Thunk
  4. Usa Redux Saga

La risposta accettata è fantastica.

Ma manca qualcosa:

  1. Nessun progetto di esempio eseguibile, solo alcuni frammenti di codice.
  2. Nessun codice di esempio per altre alternative, come:
    1. Redux Saga

Quindi ho creato il repository Hello Async per aggiungere le cose mancanti:

  1. Progetti eseguibili. Puoi scaricarli ed eseguirli senza modifiche.
  2. Fornisci un codice di esempio per ulteriori alternative:

Redux Saga

La risposta accettata fornisce già frammenti di codice di esempio per Async Code Inline, Async Action Generator e Redux Thunk. Per completezza, fornisco frammenti di codice per 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 }
}

Le azioni sono semplici e pure.

// component.js

import { connect } from 'react-redux'

// ...

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

// ...

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

Niente è speciale con il 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

Le saghe si basano su generatori ES6

// index.js

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

const sagaMiddleware = createSagaMiddleware()

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

sagaMiddleware.run(saga)

Rispetto a Redux Thunk

Professionisti

  • Non finisci nell'inferno di richiamata.
  • Puoi testare facilmente i tuoi flussi asincroni.
  • Le tue azioni rimangono pure.

Contro

  • Dipende dai generatori ES6 che sono relativamente nuovi.

Si prega di fare riferimento al progetto eseguibile se i frammenti di codice sopra non rispondono a tutte le vostre domande.


23

Puoi farlo con Redux-Thunk . C'è una guida nel documento redux per azioni asincrone come setTimeout.


Solo una domanda di follow-up rapida, quando si utilizza il middleware applyMiddleware(ReduxPromise, thunk)(createStore)è come aggiungere diversi middleware (in coma separati?) Poiché non riesco a far funzionare il thunk.
Ilja,

1
@Ilja Questo dovrebbe funzionare:const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
geniuscarrier,

22

Consiglierei anche di dare un'occhiata al modello SAM .

Il modello SAM sostiene di includere un "predicato dell'azione successiva" in cui azioni (automatiche) come "le notifiche scompaiono automaticamente dopo 5 secondi" vengono attivate una volta che il modello è stato aggiornato (modello SAM ~ stato riduttore + archivio).

Il modello sostiene le azioni di sequenziamento e le mutazioni del modello una alla volta, poiché lo "stato di controllo" del modello "controlla" quali azioni sono abilitate e / o eseguite automaticamente dal predicato dell'azione successiva. Semplicemente non puoi prevedere (in generale) quale stato sarà il sistema prima di elaborare un'azione e quindi se la tua prossima azione prevista sarà consentita / possibile.

Quindi ad esempio il codice,

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

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

non sarebbe consentito con SAM, poiché il fatto che un'azione hideNotification possa essere inviata dipende dal modello che accetta correttamente il valore "showNotication: true". Potrebbero esserci altre parti del modello che gli impediscono di accettarlo e, pertanto, non vi sarebbe motivo di innescare l'azione hideNotification.

Consiglio vivamente di implementare un predicato della prossima azione adeguato dopo che gli aggiornamenti del negozio e il nuovo stato di controllo del modello possano essere conosciuti. Questo è il modo più sicuro per attuare il comportamento che stai cercando.

Puoi unirti a noi su Gitter, se lo desideri. C'è anche una guida introduttiva SAM disponibile qui .


Finora ho solo graffiato la superficie, ma sono già elettrizzato dal modello SAM. V = S( vm( M.present( A(data) ) ), nap(M))è semplicemente bellissimo. Grazie per aver condiviso i tuoi pensieri e la tua esperienza. Scaverò più a fondo.

@ftor, grazie! quando l'ho scritto la prima volta, ho avuto la stessa sensazione. Ho usato SAM in produzione per quasi un anno ormai, e non riesco a pensare a un momento in cui sentivo di aver bisogno di una libreria per implementare SAM (anche vdom, anche se riesco a vedere quando potrebbe essere usato). Solo una riga di codice, tutto qui! SAM produce codice isomorfo, non c'è ambiguità su come gestire le chiamate asincrone ... Non riesco a pensare a un momento in cui, però, cosa sto facendo?
metaprogrammatore

SAM è un vero modello di ingegneria del software (appena prodotto un Alexa SDK con esso). Si basa su TLA + e tenta di portare la potenza di quell'incredibile lavoro a tutti gli sviluppatori. SAM corregge tre approssimazioni che (praticamente) tutti usano da decenni: - le azioni possono manipolare lo stato dell'applicazione - le assegnazioni sono equivalenti alla mutazione - non esiste una definizione precisa di cosa sia una fase di programmazione (es. A = b * ca step , sono 1 / leggi b, c 2 / calcola b * c, 3 / assegna a con il risultato tre passaggi diversi?
metaprogrammer

20

Dopo aver provato i vari approcci popolari (creatori di azioni, thunk, saghe, epopee, effetti, middleware personalizzati), ho ancora sentito che forse c'era spazio per miglioramenti, quindi ho documentato il mio viaggio in questo articolo del blog, dove inserisco la mia logica aziendale un'applicazione React / Redux?

Proprio come le discussioni qui, ho cercato di contrastare e confrontare i vari approcci. Alla fine mi ha portato a introdurre una nuova libreria redux-logic che prende ispirazione da epopee, saghe e middleware personalizzati.

Ti consente di intercettare le azioni per convalidare, verificare, autorizzare, oltre a fornire un modo per eseguire IO asincrono.

Alcune funzionalità comuni possono essere semplicemente dichiarate come rimbalzo, limitazione, cancellazione e solo utilizzando la risposta dell'ultima richiesta (takeLatest). redux-logic racchiude il tuo codice fornendo questa funzionalità per te.

Ciò ti consente di implementare la tua logica aziendale principale come preferisci. Non è necessario utilizzare osservabili o generatori a meno che non si desideri. Utilizzare funzioni e richiamate, promesse, funzioni asincrone (asincrono / attendi), ecc.

Il codice per fare una semplice notifica 5s sarebbe qualcosa del tipo:

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

Ho un esempio di notifica più avanzato nel mio repository che funziona in modo simile a quanto descritto da Sebastian Lorber in cui è possibile limitare la visualizzazione a N elementi e ruotare in uno qualsiasi di quelli in coda. esempio di notifica redux-logic

Ho una varietà di esempi live jsfiddle redux-logic, nonché esempi completi . Sto continuando a lavorare su documenti ed esempi.

Mi piacerebbe sentire il tuo feedback.


Non sono sicuro che mi piaccia la tua biblioteca ma mi piace il tuo articolo! Ben fatto, amico! Hai fatto abbastanza lavoro per risparmiare tempo agli altri.
Tyler Long,

2
Ho creato un progetto di esempio per la logica redux qui: github.com/tylerlong/hello-async/tree/master/redux-logic Penso che sia un software ben progettato e non vedo alcun grande svantaggio rispetto ad altri alternative.
Tyler Long,

9

Capisco che questa domanda è un po 'vecchia, ma ho intenzione di introdurre un'altra soluzione usando aka osservabile redux . Epico.

Citando la documentazione ufficiale:

Cosa è osservabile in redux?

Middleware basato su RxJS 5 per Redux. Comporre e annullare le azioni asincrone per creare effetti collaterali e altro ancora.

Un'epica è il primitivo fondamentale del redux-osservabile.

È una funzione che esegue un flusso di azioni e restituisce un flusso di azioni. Azioni in entrata, azioni in uscita.

In più o meno parole, è possibile creare una funzione che riceve azioni attraverso uno Stream e quindi restituire un nuovo flusso di azioni (utilizzando effetti collaterali comuni come timeout, ritardi, intervalli e richieste).

Consentitemi di pubblicare il codice e poi spiegare un po 'di più al riguardo

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)

Il codice chiave per risolvere questo problema è semplice come puoi vedere, l'unica cosa che appare diversa dalle altre risposte è la funzione rootEpic.

Punto 1. Come per le saghe, è necessario combinare le epopee per ottenere una funzione di livello superiore che riceve un flusso di azioni e restituisce un flusso di azioni, quindi è possibile utilizzarlo con la fabbrica del middleware createEpicMiddleware . Nel nostro caso ne abbiamo solo bisogno, quindi abbiamo solo il nostro rootEpic, quindi non dobbiamo combinare nulla, ma è un dato di fatto.

Punto 2. Il nostro rootEpic che si occupa della logica degli effetti collaterali richiede solo circa 5 righe di codice, il che è fantastico! Compreso il fatto che è praticamente dichiarativo!

Punto 3. Radice riga per riga Spiegazione epica (nei commenti)

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

Spero possa essere d'aiuto!


Potresti spiegare cosa stanno facendo qui i metodi specifici di API, come switchMap?
Dmitri Zaitsev

1
Stiamo utilizzando Redux-Observable nella nostra app React Native su Windows. È un'elegante soluzione di implementazione per un problema complesso, altamente asincrono e ha un supporto fantastico tramite i loro canali Gitter e GitHub. Il livello extra di complessità vale la pena solo se si arriva al problema esatto che intende risolvere, ovviamente.
Matt Hargett,

8

Perché dovrebbe essere così difficile? È solo la logica dell'interfaccia utente. Utilizzare un'azione dedicata per impostare i dati di notifica:

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

e un componente dedicato per visualizzarlo:

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

In questo caso le domande dovrebbero essere "come si pulisce il vecchio stato?", "Come notificare a un componente che l'ora è cambiata"

È possibile implementare alcune azioni TIMEOUT che vengono inviate su setTimeout da un componente.

Forse va bene pulirlo ogni volta che viene mostrata una nuova notifica.

Ad ogni modo, ci dovrebbe essere qualcosa setTimeoutda qualche parte, giusto? Perché non farlo in un componente

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

La motivazione è che la funzionalità di "dissolvenza della notifica" è davvero una preoccupazione dell'interfaccia utente. Quindi semplifica i test per la tua logica aziendale.

Non sembra logico testare la sua implementazione. Ha senso verificare solo quando deve scadere la notifica. Quindi meno codice da stub, test più veloci, codice più pulito.


1
Questa dovrebbe essere la risposta migliore.
mmla

6

Se si desidera la gestione del timeout su azioni selettive, è possibile provare il middleware approccio . Ho affrontato un problema simile per la gestione selettiva delle azioni basate sulla promessa e questa soluzione era più flessibile.

Diciamo che il tuo creatore di azioni si presenta così:

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

il timeout può contenere più valori nell'azione sopra

  • numero in ms - per una durata di timeout specifica
  • true: per una durata di timeout costante. (gestito nel middleware)
  • non definito - per la spedizione immediata

L'implementazione del middleware sarebbe simile a questa:

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

Ora puoi indirizzare tutte le tue azioni attraverso questo livello di middleware usando redux.

createStore(reducer, applyMiddleware(timeoutMiddleware))

Puoi trovare alcuni esempi simili qui


5

Il modo appropriato per farlo è usare Redux Thunk, che è un middleware popolare per Redux, secondo la documentazione di Redux Thunk:

"Il middleware Redux Thunk consente di scrivere creatori di azioni che restituiscono una funzione anziché un'azione. Il thunk può essere utilizzato per ritardare l'invio di un'azione o per inviare solo se viene soddisfatta una determinata condizione. La funzione interna riceve i metodi di archiviazione dispatch e getState come parametri ".

Quindi sostanzialmente restituisce una funzione e puoi ritardare la spedizione o metterla in uno stato di condizione.

Quindi qualcosa del genere farà il lavoro per te:

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

4

È semplice. Usa il pacchetto trim-redux e scrivi in ​​questo modo componentDidMounto in un altro posto e uccidilo componentWillUnmount.

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

componentWillUnmount() {
  clearTimeout(this.tm);
}

3

Redux stesso è una libreria piuttosto dettagliata, e per cose del genere dovresti usare qualcosa come Redux-thunk , che fornirà una dispatchfunzione, così sarai in grado di inviare la chiusura della notifica dopo alcuni secondi.

Ho creato una libreria per affrontare questioni come la verbosità e la componibilità e il tuo esempio sarà simile al seguente:

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],
});

Quindi componiamo azioni di sincronizzazione per mostrare le notifiche all'interno dell'azione asincrona, che può richiedere alcune informazioni in background o controllare in seguito se la notifica è stata chiusa manualmente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.