Fallen Sie nicht in die Falle, wenn Sie denken, eine Bibliothek sollte vorschreiben, wie alles zu tun ist . Wenn Sie etwas mit einer Zeitüberschreitung in JavaScript tun möchten, müssen Sie verwenden setTimeout
. Es gibt keinen Grund, warum Redux-Aktionen anders sein sollten.
Redux nicht bieten einige alternative Möglichkeiten, mit asynchronen Sachen zu tun, aber Sie sollten nur diejenigen verwenden , wenn Sie merken , Sie sind zu viel Code wiederholen. Wenn Sie dieses Problem nicht haben, verwenden Sie das, was die Sprache bietet, und wählen Sie die einfachste Lösung.
Async-Code inline schreiben
Dies ist bei weitem der einfachste Weg. Und hier gibt es nichts Spezielles für Redux.
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Ebenso aus dem Inneren einer verbundenen Komponente:
this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Der einzige Unterschied besteht darin, dass Sie in einer verbundenen Komponente normalerweise keinen Zugriff auf das Geschäft selbst haben, sondern entweder dispatch()
oder bestimmte Aktionsersteller als Requisiten injiziert bekommen. Dies macht jedoch für uns keinen Unterschied.
Wenn Sie beim Versenden derselben Aktionen aus verschiedenen Komponenten keine Tippfehler machen möchten, möchten Sie möglicherweise Aktionsersteller extrahieren, anstatt Aktionsobjekte inline zu versenden:
// 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)
Oder, wenn Sie sie zuvor gebunden haben mit connect()
:
this.props.showNotification('You just logged in.')
setTimeout(() => {
this.props.hideNotification()
}, 5000)
Bisher haben wir keine Middleware oder ein anderes fortschrittliches Konzept verwendet.
Async Action Creator extrahieren
Der obige Ansatz funktioniert in einfachen Fällen einwandfrei, es kann jedoch vorkommen, dass einige Probleme auftreten:
- Sie werden gezwungen, diese Logik überall dort zu duplizieren, wo Sie eine Benachrichtigung anzeigen möchten.
- Die Benachrichtigungen haben keine IDs, sodass Sie eine Rennbedingung haben, wenn Sie zwei Benachrichtigungen schnell genug anzeigen. Wenn das erste Timeout abgelaufen ist, wird es versendet
HIDE_NOTIFICATION
und die zweite Benachrichtigung wird fälschlicherweise früher als nach dem Timeout ausgeblendet.
Um diese Probleme zu lösen, müssten Sie eine Funktion extrahieren, die die Timeout-Logik zentralisiert und diese beiden Aktionen auslöst. Es könnte so aussehen:
// 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)
}
Jetzt können Komponenten verwendet werden, showNotificationWithTimeout
ohne diese Logik zu duplizieren oder Rennbedingungen mit unterschiedlichen Benachrichtigungen zu haben:
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Warum showNotificationWithTimeout()
akzeptiert dispatch
als erstes Argument? Weil es Aktionen an das Geschäft senden muss. Normalerweise hat eine Komponente Zugriff auf, dispatch
aber da eine externe Funktion die Kontrolle über den Versand übernehmen soll, müssen wir ihr die Kontrolle über den Versand geben.
Wenn Sie einen Singleton-Speicher aus einem Modul exportieren ließen, können Sie ihn einfach importieren und dispatch
stattdessen direkt darauf:
// 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.')
Das sieht einfacher aus, aber wir empfehlen diesen Ansatz nicht . Der Hauptgrund, warum wir es nicht mögen, ist, dass es den Speicher zwingt, ein Singleton zu sein . Dies macht es sehr schwierig, das Server-Rendering zu implementieren . Auf dem Server soll jede Anforderung einen eigenen Speicher haben, damit verschiedene Benutzer unterschiedliche vorinstallierte Daten erhalten.
Ein Singleton-Store erschwert auch das Testen. Sie können einen Speicher nicht mehr verspotten, wenn Sie Aktionsersteller testen, da diese auf einen bestimmten realen Speicher verweisen, der aus einem bestimmten Modul exportiert wurde. Sie können den Status nicht einmal von außen zurücksetzen.
Obwohl Sie einen Singleton-Speicher technisch aus einem Modul exportieren können, raten wir davon ab. Tun Sie dies nur, wenn Sie sicher sind, dass Ihre App niemals Server-Rendering hinzufügt.
Zurück zur vorherigen Version:
// 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.')
Dies löst die Probleme mit der Verdoppelung der Logik und bewahrt uns vor den Rennbedingungen.
Thunk Middleware
Für einfache Apps sollte der Ansatz ausreichen. Machen Sie sich keine Sorgen um Middleware, wenn Sie damit zufrieden sind.
In größeren Apps können jedoch bestimmte Unannehmlichkeiten auftreten.
Zum Beispiel scheint es bedauerlich, dass wir herumreichen müssen dispatch
. Dies macht es schwieriger, Container- und Präsentationskomponenten zu trennen, da jede Komponente, die Redux-Aktionen auf die oben beschriebene Weise asynchron auslöst, dispatch
als Requisite akzeptiert werden muss, damit sie sie weitergeben kann. Sie können nicht mehr nur Aktionsersteller binden, connect()
da dies showNotificationWithTimeout()
nicht wirklich ein Aktionsersteller ist. Es wird keine Redux-Aktion zurückgegeben.
Darüber hinaus kann es schwierig sein, sich daran zu erinnern, welche Funktionen synchrone Aktionsersteller showNotification()
und welche asynchrone Helfer sind showNotificationWithTimeout()
. Sie müssen sie anders verwenden und darauf achten, sie nicht miteinander zu verwechseln.
Dies war die Motivation , einen Weg zu finden, um dieses Muster der Bereitstellung dispatch
einer Hilfsfunktion zu „legitimieren“ und Redux dabei zu helfen, solche asynchronen Aktionsersteller als Sonderfall normaler Aktionsersteller und nicht als völlig andere Funktionen zu „sehen“ .
Wenn Sie noch bei uns sind und auch ein Problem in Ihrer App erkennen, können Sie gerne die Redux Thunk- Middleware verwenden.
Kurz gesagt, Redux Thunk lehrt Redux, spezielle Arten von Aktionen zu erkennen, die tatsächlich Funktionen sind:
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)
})
Wenn diese Middleware aktiviert ist und Sie eine Funktion versenden , gibt die Redux Thunk-Middleware diese dispatch
als Argument an. Es wird auch solche Aktionen "schlucken", also machen Sie sich keine Sorgen, dass Ihre Reduzierer seltsame Funktionsargumente erhalten. Ihre Reduzierungen erhalten nur einfache Objektaktionen - entweder direkt oder von den gerade beschriebenen Funktionen.
Das sieht nicht sehr nützlich aus, oder? Nicht in dieser besonderen Situation. Wir können uns jedoch showNotificationWithTimeout()
als regulärer Redux-Aktionsersteller deklarieren :
// 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)
}
}
Beachten Sie, dass die Funktion fast identisch mit der im vorherigen Abschnitt beschriebenen ist. Es wird jedoch nicht dispatch
als erstes Argument akzeptiert . Stattdessen es gibt eine Funktion , die übernimmt dispatch
als erstes Argument.
Wie würden wir es in unserer Komponente verwenden? Auf jeden Fall könnten wir das schreiben:
// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)
Wir rufen den asynchronen Aktionsersteller auf, um die innere Funktion zu erhalten, die gerecht sein soll dispatch
, und dann bestehen wir dispatch
.
Dies ist jedoch noch umständlicher als die Originalversion! Warum sind wir überhaupt diesen Weg gegangen?
Wegen dem, was ich dir vorher gesagt habe. Wenn die Redux Thunk-Middleware aktiviert ist, ruft die Middleware jedes Mal, wenn Sie versuchen, eine Funktion anstelle eines Aktionsobjekts auszulösen, diese Funktion mit der dispatch
Methode selbst als erstem Argument auf .
Also können wir dies stattdessen tun:
// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
Schließlich sieht das Versenden einer asynchronen Aktion (wirklich eine Reihe von Aktionen) nicht anders aus als das Versenden einer einzelnen Aktion synchron zur Komponente. Das ist gut, weil es Komponenten egal sein sollte, ob etwas synchron oder asynchron passiert. Wir haben das einfach weg abstrahiert.
Da wir Redux „beigebracht“ haben, solche „speziellen“ Aktionsersteller zu erkennen (wir nennen sie Thunk- Aktionsersteller), können wir sie jetzt an jedem Ort verwenden, an dem wir reguläre Aktionsersteller verwenden würden. Zum Beispiel können wir sie verwenden mit 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)
Lesestatus in Thunks
Normalerweise enthalten Ihre Reduzierungen die Geschäftslogik zur Bestimmung des nächsten Status. Reduzierungen werden jedoch erst aktiviert, nachdem die Aktionen ausgelöst wurden. Was ist, wenn Sie einen Nebeneffekt (z. B. das Aufrufen einer API) in einem Thunk-Aktionsersteller haben und diesen unter bestimmten Bedingungen verhindern möchten?
Ohne die Thunk-Middleware würden Sie nur diese Überprüfung innerhalb der Komponente durchführen:
// component.js
if (this.props.areNotificationsEnabled) {
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}
Beim Extrahieren eines Aktionserstellers ging es jedoch darum, diese sich wiederholende Logik über viele Komponenten hinweg zu zentralisieren. Glücklicherweise bietet Ihnen Redux Thunk eine Möglichkeit, den aktuellen Status des Redux-Stores zu lesen . Darüber hinaus dispatch
wird es getState
als zweites Argument an die Funktion übergeben, die Sie von Ihrem Thunk-Aktionsersteller zurückgeben. Dadurch kann der Thunk den aktuellen Status des Geschäfts lesen.
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)
}
}
Missbrauche dieses Muster nicht. Es ist gut, um API-Aufrufe zu beenden, wenn zwischengespeicherte Daten verfügbar sind, aber es ist keine sehr gute Grundlage, um auf Ihrer Geschäftslogik aufzubauen. Wenn Sie getState()
nur verschiedene Aktionen bedingt auslösen, sollten Sie stattdessen die Geschäftslogik in die Reduzierungen einfügen.
Nächste Schritte
Nachdem Sie eine grundlegende Vorstellung davon haben, wie Thunks funktionieren, sehen Sie sich das asynchrone Redux- Beispiel an, in dem sie verwendet werden.
Möglicherweise finden Sie viele Beispiele, in denen Thunks Versprechen zurückgeben. Dies ist nicht erforderlich, kann aber sehr praktisch sein. Redux ist es egal, was Sie von einem Thunk zurückgeben, aber es gibt Ihnen den Rückgabewert von dispatch()
. Aus diesem Grund können Sie ein Versprechen von einem Thunk zurückgeben und warten, bis es abgeschlossen ist, indem Sie anrufen dispatch(someThunkReturningPromise()).then(...)
.
Sie können komplexe Thunk-Action-Ersteller auch in mehrere kleinere Thunk-Action-Ersteller aufteilen. Die dispatch
von Thunks bereitgestellte Methode kann Thunks selbst akzeptieren, sodass Sie das Muster rekursiv anwenden können. Auch dies funktioniert am besten mit Promises, da Sie darüber hinaus einen asynchronen Steuerungsfluss implementieren können.
Bei einigen Apps befinden Sie sich möglicherweise in einer Situation, in der Ihre Anforderungen an den asynchronen Steuerungsfluss zu komplex sind, um mit Thunks ausgedrückt zu werden. Beispielsweise kann das Wiederholen fehlgeschlagener Anforderungen, der erneute Autorisierungsfluss mit Token oder ein schrittweises Onboarding zu ausführlich und fehleranfällig sein, wenn es auf diese Weise geschrieben wird. In diesem Fall sollten Sie sich erweiterte asynchrone Steuerungsflusslösungen wie Redux Saga oder Redux Loop ansehen . Bewerten Sie sie, vergleichen Sie die für Ihre Bedürfnisse relevanten Beispiele und wählen Sie das aus, das Ihnen am besten gefällt.
Verwenden Sie schließlich nichts (einschließlich Thunks), wenn Sie nicht wirklich das Bedürfnis danach haben. Denken Sie daran, dass Ihre Lösung je nach den Anforderungen so einfach aussehen kann wie
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Schwitzen Sie nicht, es sei denn, Sie wissen, warum Sie das tun.
redux-saga
Antwort zu überprüfen , wenn du etwas Besseres als Thunks willst. Eine späte Antwort, sodass Sie lange scrollen müssen, bevor Sie sie sehen können :) bedeutet nicht, dass es nicht lesenswert ist. Hier ist eine Verknüpfung: stackoverflow.com/a/38574266/82609