it-swarm.com.de

Wie versende ich eine Redux-Aktion mit einer Zeitüberschreitung?

Ich habe eine Aktion, die den Benachrichtigungsstatus meiner Anwendung aktualisiert. Normalerweise handelt es sich bei dieser Benachrichtigung um einen Fehler oder eine Information. Ich muss dann nach 5 Sekunden eine weitere Aktion auslösen, die den Benachrichtigungsstatus auf den ursprünglichen Status zurücksetzt, also keine Benachrichtigung. Der Hauptgrund dafür ist die Bereitstellung von Funktionen, bei denen Benachrichtigungen nach 5 Sekunden automatisch ausgeblendet werden.

Ich hatte kein Glück damit, setTimeout zu verwenden und eine andere Aktion zurückzugeben, und kann nicht herausfinden, wie dies online gemacht wird. Daher ist jeder Rat willkommen.

793
Ilja

Gehen Sie nicht in die Falle, wenn Sie denken, eine Bibliothek sollte vorschreiben, wie alles zu tun ist . Wenn Sie mit einem Timeout in JavaScript etwas anfangen möchten, müssen Sie setTimeout verwenden. Es gibt keinen Grund, warum Redux-Aktionen anders sein sollten.

Redux does bietet einige alternative Möglichkeiten für den Umgang mit asynchronen Inhalten. Sie sollten diese jedoch nur verwenden, wenn Sie feststellen, dass Sie 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.

Schreiben von asynchronem Code inline

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)

Ähnlich verhält es sich innerhalb 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 erhalten. Dies macht jedoch keinen Unterschied für uns.

Wenn Sie keine Tippfehler machen möchten, wenn Sie dieselben Aktionen aus verschiedenen Komponenten auslösen, möchten Sie möglicherweise Aktionsersteller extrahieren, anstatt Aktionsobjekte inline auszulösen:

// 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 mit connect() gebunden haben:

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

Bisher haben wir keine Middleware oder ein anderes fortschrittliches Konzept verwendet.

Extrahieren von Async Action Creator

Der obige Ansatz funktioniert in einfachen Fällen einwandfrei, es können jedoch einige Probleme auftreten:

  • Sie werden gezwungen, diese Logik überall dort zu duplizieren, wo Sie eine Benachrichtigung anzeigen möchten.
  • Die Benachrichtigungen haben keine IDs. Sie haben also eine Rennbedingung, wenn Sie zwei Benachrichtigungen schnell genug anzeigen. Wenn die erste Zeitüberschreitung abgelaufen ist, wird HIDE_NOTIFICATION gesendet, wodurch die zweite Benachrichtigung fälschlicherweise früher als nach der Zeitüberschreitung ausgeblendet wird.

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 showNotificationWithTimeout verwenden, ohne diese Logik zu duplizieren oder Race-Bedingungen 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 akzeptiert showNotificationWithTimeout()dispatch als erstes Argument? Weil es Aktionen an den Laden senden muss. Normalerweise hat eine Komponente Zugriff auf dispatch, aber da wir möchten, dass eine externe Funktion die Kontrolle über das Disponieren übernimmt, müssen wir ihr die Kontrolle über das Disponieren geben.

Wenn Sie einen Singleton-Speicher aus einem Modul exportiert haben, können Sie ihn einfach importieren und stattdessen dispatch 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 erzwingt, dass der Speicher ein Singleton ist . Dies macht es sehr schwierig, Server-Rendering zu implementieren. Auf dem Server soll jede Anforderung einen eigenen Speicher haben, damit verschiedene Benutzer unterschiedliche vorab geladene Daten erhalten.

Ein Einzelhandelsgeschäft erschwert auch das Testen. Sie können ein Geschäft nicht mehr verspotten, wenn Sie Aktionsersteller testen, da sie auf ein bestimmtes reales Geschäft verweisen, das 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 Vervielfältigung der Logik und erspart uns die 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 kann es jedoch zu gewissen Unannehmlichkeiten kommen.

Zum Beispiel scheint es bedauerlich, dass wir dispatch herumreichen müssen. 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 akzeptieren muss, damit sie diese weiterleiten kann. Sie können Aktionsersteller nicht mehr nur mit connect() binden, da showNotificationWithTimeout() eigentlich kein Aktionsersteller ist. Es wird keine Redux-Aktion zurückgegeben.

Außerdem kann es schwierig sein, sich daran zu erinnern, welche Funktionen Synchronaktionsersteller wie showNotification() und welche asynchrone Helfer wie showNotificationWithTimeout() sind. Sie müssen sie anders verwenden und darauf achten, sie nicht miteinander zu verwechseln.

Dies war die Motivation , um dieses Muster der Bereitstellung von dispatch für eine Hilfsfunktion zu "legitimieren" und Redux dabei zu helfen, solche Ersteller von asynchronen Aktionen als Sonderfall von "zu sehen" normale Aktionsersteller anstatt völlig andere Funktionen.

Wenn Sie immer noch bei uns sind und auch ein Problem in Ihrer App erkennen, können Sie gerne die Redux Thunk Middleware verwenden.

In einem Gist lehrt Redux Thunk Redux, bestimmte Arten von Aktionen zu erkennen, die tatsächlich funktionieren:

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 auslösen , gibt die Redux Thunk-Middleware dispatch als Argument an. Es wird auch solche Aktionen "verschlucken", sodass Sie sich keine Sorgen machen müssen, dass Ihre Reduzierungen seltsame Funktionsargumente erhalten. Ihre Reduzierungen erhalten nur einfache Objektaktionen - entweder direkt oder von den oben beschriebenen Funktionen.

Das sieht nicht sehr nützlich aus, oder? Nicht in dieser besonderen Situation. Allerdings können wir showNotificationWithTimeout() als regulären 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 mit der im vorherigen Abschnitt beschriebenen Funktion fast identisch ist. Es akzeptiert jedoch nicht dispatch als erstes Argument. Stattdessen gibteine Funktion zurück, die dispatch als erstes Argument akzeptiert.

Wie würden wir es in unserer Komponente verwenden? Auf jeden Fall könnten wir folgendes schreiben:

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

Wir rufen den Async Action Creator auf, um die innere Funktion zu erhalten, die nur dispatch will, und übergeben dann 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 bei jedem Versuch, eine Funktion anstelle eines Aktionsobjekts auszulösen, diese Funktion mit der dispatch -Methode selbst als erstem Argument auf .

Also können wir das stattdessen machen:

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

Schließlich unterscheidet sich das Auslösen einer asynchronen Aktion (also einer Reihe von Aktionen) nicht vom synchronen Auslösen einer einzelnen Aktion für die Komponente. Das ist gut, weil es den Komponenten egal sein sollte, ob etwas synchron oder asynchron geschieht. Wir haben das einfach weggezogen.

Beachten Sie, dass wir Redux "beigebracht" haben, solche "speziellen" Aktionsersteller zu erkennen (wir nennen sie thunk Aktionsersteller), und dass wir sie jetzt an jedem Ort verwenden können, an dem wir normale Aktionsersteller einsetzen würden. Zum Beispiel können wir sie mit connect() verwenden:

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

Lesezustand in Thunks

Normalerweise enthalten Ihre Reduktionen die Geschäftslogik zur Bestimmung des nächsten Zustands. Reduzierungen treten jedoch erst nach dem Auslösen der Aktionen ein. Was ist, wenn Sie in einem Thunk-Action-Ersteller einen Nebeneffekt haben (z. B. das Aufrufen einer API) und diesen unter bestimmten Umständen verhindern möchten?

Ohne Verwendung der Thunk-Middleware führen Sie einfach diese Überprüfung in der Komponente durch:

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

Der Zweck des Extrahierens eines Aktionserstellers bestand jedoch darin, diese sich wiederholende Logik über viele Komponenten hinweg zu zentralisieren. Glücklicherweise bietet Ihnen Redux Thunk die Möglichkeit, read den aktuellen Status des Redux-Stores zu ermitteln. Neben dispatch wird getState als zweites Argument an die Funktion übergeben, die Sie von Ihrem Thunk-Aktionsersteller zurückgeben. Dadurch kann der Thunk den aktuellen Status des Speichers 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 zum bedingten Versenden verschiedener Aktionen verwenden, sollten Sie stattdessen die Geschäftslogik in die Reduzierungen einfügen.

Nächste Schritte

Nachdem Sie eine grundlegende Vorstellung davon haben, wie Thunks funktionieren, lesen Sie Redux async example , das sie verwendet.

Sie können viele Beispiele finden, 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 dispatch(someThunkReturningPromise()).then(...) aufrufen.

Sie können auch komplexe Thunk-Action-Ersteller in mehrere kleinere Thunk-Action-Ersteller aufteilen. Die von thunks bereitgestellte dispatch -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. Das Wiederholen fehlgeschlagener Anforderungen, das erneute Autorisieren mit Token oder ein schrittweises Onboarding können beispielsweise zu ausführlich und fehleranfällig sein, wenn sie auf diese Weise geschrieben werden. In diesem Fall möchten Sie möglicherweise erweiterte asynchrone Steuerungsflusslösungen wie Redux Saga oder Redux Loop anschauen. Bewerten Sie sie, vergleichen Sie die für Ihre Bedürfnisse relevanten Beispiele und wählen Sie die aus, die Ihnen am besten gefällt.

Verwenden Sie zum Schluss nichts (einschließlich Thunks), wenn Sie nicht das echte Bedürfnis danach haben. Denken Sie daran, dass Ihre Lösung je nach Anforderung so einfach wie möglich aussehen kann

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

Schwitzen Sie nur, wenn Sie wissen, warum Sie dies tun.

2407
Dan Abramov

Mit Redux-Saga

Wie Dan Abramov sagte, wenn Sie eine erweiterte Kontrolle über Ihren Async-Code wünschen, sollten Sie sich redux-saga ansehen.

Diese Antwort ist ein einfaches Beispiel. Wenn Sie bessere Erklärungen wünschen, warum Redux-Saga für Ihre Anwendung nützlich sein kann, überprüfen Sie diese andere Antwort .

Die allgemeine Idee ist, dass Redux-saga einen ES6-Generatoren-Interpreter bietet, mit dem Sie einfach asynchronen Code schreiben können, der wie synchroner Code aussieht (aus diesem Grund finden Sie in Redux-saga häufig unendlich viele while-Schleifen). Irgendwie baut die Redux-Saga ihre eigene Sprache direkt in Javascript auf. Redux-Saga kann sich zunächst etwas schwierig anfühlen, da Sie Grundkenntnisse über Generatoren benötigen, aber auch die von Redux-Saga angebotene Sprache verstehen.

Ich werde hier versuchen, das Benachrichtigungssystem zu beschreiben, das ich auf der Redux-Saga aufgebaut habe. Dieses Beispiel wird derzeit in der Produktion ausgeführt.

Erweiterte Spezifikation des Benachrichtigungssystems

  • Sie können die Anzeige einer Benachrichtigung anfordern
  • Sie können eine Benachrichtigung zum Ausblenden anfordern
  • Eine Benachrichtigung sollte nicht länger als 4 Sekunden angezeigt werden
  • Es können mehrere Benachrichtigungen gleichzeitig angezeigt werden
  • Es können nicht mehr als 3 Benachrichtigungen gleichzeitig angezeigt werden
  • Wenn eine Benachrichtigung angefordert wird, während bereits 3 Benachrichtigungen angezeigt werden, wird sie in die Warteschlange gestellt bzw. verschoben.

Ergebnis

Screenshot meiner Produktions-App Stample.co

toasts

Code

Hier habe ich die Benachrichtigung als toast bezeichnet, aber dies ist ein Namensdetail.

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

Und der Reduzierer:

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

Verwendungszweck

Sie können einfach TOAST_DISPLAY_REQUESTED Ereignisse auslösen. Wenn Sie 4 Anfragen versenden, werden nur 3 Benachrichtigungen angezeigt und die 4. wird etwas später angezeigt, sobald die 1. Benachrichtigung verschwindet.

Beachten Sie, dass ich nicht ausdrücklich empfehle, TOAST_DISPLAY_REQUESTED von JSX aus zu versenden. Sie möchten lieber eine weitere Saga hinzufügen, die Ihre bereits vorhandenen App-Ereignisse abhört, und dann den TOAST_DISPLAY_REQUESTED auslösen: Ihre Komponente, die die Benachrichtigung auslöst, muss nicht eng mit dem Benachrichtigungssystem verbunden sein.

Fazit

Mein Code ist nicht perfekt, läuft aber monatelang mit 0 Bugs in der Produktion. Redux-Saga und Generatoren sind anfangs etwas schwierig, aber wenn man sie erst einmal versteht, ist diese Art von System ziemlich einfach zu bauen.

Es ist sogar ganz einfach, komplexere Regeln zu implementieren, wie zum Beispiel:

  • wenn sich zu viele Benachrichtigungen in der Warteschlange befinden, geben Sie für jede Benachrichtigung weniger Zeit für die Anzeige, damit die Größe der Warteschlange schneller abnimmt.
  • ermitteln Sie Änderungen der Fenstergröße und ändern Sie die maximale Anzahl der angezeigten Benachrichtigungen entsprechend (z. B. Desktop = 3, Telefonporträt = 2, Telefonlandschaft = 1).

Ehrlich gesagt, viel Glück beim richtigen Implementieren dieser Art von Dingen mit Thunks.

Beachten Sie, dass Sie mit Redux-Observable genau dasselbe tun können, was der Redux-Saga sehr ähnlich ist. Es ist fast dasselbe und Geschmackssache zwischen Generatoren und RxJS.

170

Ein Repository mit Beispielprojekten

Derzeit gibt es vier Beispielprojekte:

  1. Schreiben von Async Code Inline
  2. Extrahieren von Async Action Creator
  3. Benutze Redux Thunk
  4. Benutze Redux Saga

Die akzeptierte Antwort ist fantastisch.

Aber es fehlt etwas:

  1. Keine ausführbaren Beispielprojekte, nur einige Code-Schnipsel.
  2. Kein Beispielcode für andere Alternativen, z. B .:
    1. Redux Saga

Also habe ich das Hello Async Repository erstellt, um die fehlenden Dinge hinzuzufügen:

  1. Lauffähige Projekte. Sie können sie ohne Änderungen herunterladen und ausführen.
  2. Stellen Sie Beispielcode für weitere Alternativen bereit:

Redux Saga

Die akzeptierte Antwort enthält bereits Codebeispiele für Async Code Inline, Async Action Generator und Redux Thunk. Der Vollständigkeit halber stelle ich Codefragmente für Redux Saga zur Verfügung:

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

Aktionen sind einfach und rein.

// component.js

import { connect } from 'react-redux'

// ...

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

// ...

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

Mit der Komponente ist nichts Besonderes.

// 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 basieren auf ES6-Generatoren

// index.js

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

const sagaMiddleware = createSagaMiddleware()

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

sagaMiddleware.run(saga)

Im Vergleich zu Redux Thunk

Vorteile

  • Sie landen nicht in der Rückruf-Hölle.
  • Sie können Ihre asynchronen Abläufe problemlos testen.
  • Deine Handlungen bleiben rein.

Nachteile

  • Es hängt von ES6-Generatoren ab, was relativ neu ist.

Bitte lesen Sie das lauffähiges Projekt , wenn die obigen Code-Schnipsel nicht alle Ihre Fragen beantworten.

21
Tyler Long

Ich würde auch empfehlen, sich das SAM-Muster anzuschauen.

Das SAM-Muster befürwortet die Aufnahme eines "Next-Action-Prädikats", bei dem (automatische) Aktionen wie "Benachrichtigungen verschwinden automatisch nach 5 Sekunden" ausgelöst werden, sobald das Modell aktualisiert wurde (SAM-Modell ~ Reduzierer-Status + Speicher).

Das Muster befürwortet, Aktionen und Modellmutationen nacheinander zu sequenzieren, da der "Steuerzustand" des Modells "steuert", welche Aktionen vom Prädikat der nächsten Aktion aktiviert und/oder automatisch ausgeführt werden. Sie können (im Allgemeinen) einfach nicht vorhersagen, in welchem ​​Zustand sich das System befindet, bevor eine Aktion ausgeführt wird, und daher, ob Ihre nächste erwartete Aktion zulässig/möglich ist.

So zum Beispiel der Code,

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

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

wäre mit SAM nicht zulässig, da die Tatsache, dass eine hideNotification-Aktion ausgelöst werden kann, davon abhängt, ob das Modell den Wert "showNotication: true" erfolgreich akzeptiert. Es könnte andere Teile des Modells geben, die es daran hindern, es zu akzeptieren, und daher gäbe es keinen Grund, die Aktion hideNotification auszulösen.

Es wird dringend empfohlen, ein geeignetes Prädikat für die nächste Aktion zu implementieren, nachdem die Speicheraktualisierungen und der neue Steuerungsstatus des Modells bekannt sind. Dies ist der sicherste Weg, um das von Ihnen gesuchte Verhalten umzusetzen.

Sie können sich uns auf Gitter anschließen, wenn Sie möchten. Es gibt auch eine SAM Erste Schritte Anleitung hier verfügbar .

19

Sie können dies mit Redux-Thunk tun. Es gibt eine Anleitung im Redux-Dokument für asynchrone Aktionen wie setTimeout.

19
Fatih Erikli

Nachdem ich die verschiedenen populären Ansätze ausprobiert hatte (Actionkünstler, Thunks, Sagen, Epen, Effekte, benutzerdefinierte Middleware), hatte ich immer noch das Gefühl, dass es möglicherweise Verbesserungspotenzial gibt, und habe meine Reise in diesem Blogartikel dokumentiert Wo lege ich meine Geschäftslogik in einer React/Redux-Anwendung ab?

Ähnlich wie bei den Diskussionen hier habe ich versucht, die verschiedenen Ansätze gegenüberzustellen und zu vergleichen. Schließlich führte es mich zur Einführung einer neuen Bibliothek mit Redux-Logik , die sich von Epen, Sagen und angepasster Middleware inspirieren ließ.

Sie können damit Aktionen abfangen, die validieren, verifizieren, autorisieren und die asynchrone E/A-Verarbeitung ermöglichen.

Einige gängige Funktionen können einfach deklariert werden, z. B. Entprellen, Drosseln, Abbrechen und nur unter Verwendung der Antwort der letzten Anforderung (takeLatest). redux-logic umschließt Ihren Code und stellt diese Funktionalität für Sie bereit.

So können Sie Ihre Kerngeschäftslogik nach Belieben implementieren. Sie müssen keine Observablen oder Generatoren verwenden, es sei denn, Sie möchten. Verwenden Sie Funktionen und Rückrufe, Versprechen, asynchrone Funktionen (async/await) usw.

Der Code für eine einfache 5s-Benachrichtigung lautet etwa:

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

Ich habe ein erweitertes Benachrichtigungsbeispiel in meinem Repo, das ähnlich funktioniert wie das, was Sebastian Lorber beschrieben hat, bei dem Sie die Anzeige auf N Elemente beschränken und alle in der Warteschlange befindlichen Elemente durchlaufen können. Beispiel für eine Benachrichtigung mit Redux-Logik

Ich habe eine Vielzahl von Redux-Logik jsfiddle Live-Beispielen sowie vollständige Beispiele . Ich arbeite weiterhin an Dokumenten und Beispielen.

Ich würde gerne Ihr Feedback hören.

17
Jeff Barczewski

Ich verstehe, dass diese Frage ein bisschen alt ist, aber ich werde eine andere Lösung mit redux-observable aka vorstellen. Epos.

Zitieren der offiziellen Dokumentation:

Was ist Redux-Observable?

RxJS 5-basierte Middleware für Redux. Erstellen Sie und brechen Sie asynchrone Aktionen ab, um Nebenwirkungen und mehr zu verursachen.

Ein Epos ist das Kernelement von Redux-Observable.

Es ist eine Funktion, die eine Reihe von Aktionen ausführt und eine Reihe von Aktionen zurückgibt. Aktionen rein, Aktionen raus.

Mit mehr oder weniger Worten können Sie eine Funktion erstellen, die Aktionen über einen Stream empfängt und anschließend einen neuen Stream von Aktionen zurückgibt (wobei häufig auftretende Nebenwirkungen wie Zeitüberschreitungen, Verzögerungen, Intervalle und Anforderungen verwendet werden).

Lassen Sie mich den Code posten und dann etwas näher erläutern

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)

Der Schlüsselcode zur Lösung dieses Problems ist so einfach wie möglich. Das einzige, was sich von den anderen Antworten unterscheidet, ist die Funktion rootEpic.

Punkt 1. Wie bei Sagen müssen Sie die Epen kombinieren, um eine Top-Level-Funktion zu erhalten, die einen Stream von Aktionen empfängt und einen Stream von Aktionen zurückgibt, sodass Sie sie mit der Middleware-Factory verwenden können . createEpicMiddleware . In unserem Fall brauchen wir nur einen, also haben wir nur unseren rootEpic , damit wir nichts kombinieren müssen, aber es ist eine gute Tatsache zu wissen.

Punkt 2. Unser rootEpic , das sich um die Logik der Nebenwirkungen kümmert, benötigt nur ungefähr 5 Codezeilen, was fantastisch ist! Einschließlich der Tatsache, dass ziemlich aussagekräftig ist!

Punkt 3. Zeile für Zeile rootEpic Erklärung (in Kommentaren)

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

Ich hoffe, es hilft!

7
cnexans

Warum sollte es so schwer sein? Es ist nur UI-Logik. Verwenden Sie eine dedizierte Aktion, um Benachrichtigungsdaten festzulegen:

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

und eine dedizierte Komponente, um es anzuzeigen:

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

In diesem Fall lauten die Fragen "Wie wird der alte Zustand bereinigt?", "Wie wird eine Komponente benachrichtigt, wenn sich die Zeit geändert hat".

Sie können eine TIMEOUT-Aktion implementieren, die bei setTimeout von einer Komponente ausgelöst wird.

Vielleicht ist es in Ordnung, es zu reinigen, wenn eine neue Benachrichtigung angezeigt wird.

Wie auch immer, irgendwo sollte es etwas setTimeout geben, oder? Warum nicht in einer Komponente

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

Die Motivation ist, dass die Funktion "Benachrichtigung ausblenden" wirklich ein Problem für die Benutzeroberfläche ist. Dies vereinfacht das Testen Ihrer Geschäftslogik.

Es scheint keinen Sinn zu machen, zu testen, wie es implementiert ist. Es ist nur sinnvoll zu überprüfen, wann die Benachrichtigung abläuft. So weniger Code zu stub, schnellere Tests, sauberer Code.

5
Vanuan

Wenn Sie Zeitlimitüberschreitungen bei ausgewählten Aktionen behandeln möchten, können Sie den Ansatz Middleware verwenden. Ich hatte ein ähnliches Problem damit, versprechensbasierte Aktionen selektiv zu handhaben, und diese Lösung war flexibler.

Nehmen wir an, Ihr Action-Ersteller sieht folgendermaßen aus:

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

das Zeitlimit kann in der obigen Aktion mehrere Werte enthalten

  • zahl in ms - für eine bestimmte Timeout-Dauer
  • true - für eine konstante Timeout-Dauer. (in der Middleware gehandhabt)
  • undefined - zum sofortigen Versand

Ihre Middleware-Implementierung würde folgendermaßen aussehen:

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

Sie können jetzt alle Ihre Aktionen mithilfe von Redux durch diese Middleware-Schicht leiten.

createStore(reducer, applyMiddleware(timeoutMiddleware))

Sie können einige ähnliche Beispiele finden hier

5
Yash

Der geeignete Weg, dies zu tun, ist die Verwendung von Redux Thunk, einer beliebten Middleware für Redux, wie in der Redux Thunk-Dokumentation beschrieben:

"Mit der Redux Thunk Middleware können Sie Aktionsersteller schreiben, die eine Funktion anstelle einer Aktion zurückgeben. Mit dem Thunk können Sie den Versand einer Aktion verzögern oder nur versenden, wenn eine bestimmte Bedingung erfüllt ist. Die innere Funktion empfängt die Speichermethoden dispatch und getState als Parameter ".

Im Grunde gibt es eine Funktion zurück und Sie können Ihren Versand verzögern oder in einen Zustand versetzen.

So etwas erledigt die Arbeit für Sie:

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

Es ist einfach. Verwenden Sie trim-redux package und schreiben Sie so in componentDidMount oder einen anderen Ort und töten Sie ihn in componentWillUnmount.

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

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

Redux selbst ist eine ziemlich ausführliche Bibliothek, und für solche Dinge müsste man so etwas wie Redux-thunk verwenden, das eine dispatch -Funktion gibt, damit Sie das Schließen von versenden können die Benachrichtigung nach einigen Sekunden.

Ich habe eine Bibliothek erstellt um Themen wie Ausführlichkeit und Kompositionsfähigkeit zu behandeln, und Ihr Beispiel sieht folgendermaßen aus:

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

Daher erstellen wir Synchronisierungsaktionen, um Benachrichtigungen in einer asynchronen Aktion anzuzeigen, die Informationen zum Hintergrund abrufen oder später überprüfen kann, ob die Benachrichtigung manuell geschlossen wurde.

1
Bloomca