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 showNotificationWithTimeout
senza 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 dispatch
come primo argomento? Perché deve inviare le azioni al negozio. Normalmente un componente ha accesso, dispatch
ma 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 dispatch
direttamente 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 dispatch
giro. 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 dispatch
come 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 dispatch
a 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à dispatch
come 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 dispatch
come primo argomento. Invece restituisce una funzione che accetta dispatch
come 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 dispatch
metodo 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 getState
come 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 dispatch
metodo 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.
redux-saga
risposta 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