Question Comment envoyer une action Redux avec un timeout?


J'ai une action qui met à jour l'état de notification de mon application. Habituellement, cette notification sera une erreur ou une information de quelque sorte. Je dois ensuite envoyer une autre action après 5 secondes qui retournera l'état de notification à l'initiale, donc pas de notification. La raison principale derrière ceci est de fournir une fonctionnalité où les notifications disparaissent automatiquement après 5 secondes.

Je n'avais pas de chance d'utiliser setTimeout et retourner une autre action et ne peut pas trouver comment cela se fait en ligne. Donc, tout conseil est le bienvenu.


628
2018-02-15 14:03


origine


Réponses:


Ne tombez pas dans le piège de penser à une bibliothèque devrait prescrire comment tout faire. Si vous voulez faire quelque chose avec un timeout en JavaScript, vous devez utiliser setTimeout. Il n'y a aucune raison pour que les actions Redux soient différentes.

Redux Est-ce que offrir d'autres façons de traiter les choses asynchrones, mais vous ne devriez les utiliser que lorsque vous réalisez que vous répétez trop de code. À moins d'avoir ce problème, utilisez ce que la langue offre et optez pour la solution la plus simple.

Écriture du code asynchrone en ligne

C'est de loin le moyen le plus simple. Et il n'y a rien de spécifique à Redux ici.

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

De même, depuis l'intérieur d'un composant connecté:

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

La seule différence est que dans un composant connecté, vous n'avez généralement pas accès au magasin lui-même, mais obtenez soit dispatch() ou des créateurs d'actions spécifiques injectés comme accessoires. Cependant, cela ne fait aucune différence pour nous.

Si vous n'aimez pas faire des fautes de frappe lors de l'envoi des mêmes actions à partir de différents composants, vous pouvez extraire des créateurs d'actions au lieu de distribuer des objets d'action en ligne:

// actions.js
export function showNotification(text) {
  return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
  return { type: 'HIDE_NOTIFICATION' }
}

// component.js
import { showNotification, hideNotification } from '../actions'

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

Ou, si vous les avez déjà liés avec connect():

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

Jusqu'à présent, nous n'avons utilisé aucun middleware ou autre concept avancé.

Extraction du créateur d'action asynchrone

L'approche ci-dessus fonctionne bien dans les cas simples, mais vous pourriez trouver qu'il a quelques problèmes:

  • Cela vous oblige à dupliquer cette logique partout où vous voulez afficher une notification.
  • Les notifications n'ont pas d'ID, vous aurez donc une condition de concurrence si vous affichez deux notifications assez rapidement. Lorsque le premier délai expire, il envoie HIDE_NOTIFICATION, masquant à tort la deuxième notification plus tôt qu'après le timeout.

Pour résoudre ces problèmes, vous devez extraire une fonction qui centralise la logique de délai et distribue ces deux actions. Cela pourrait ressembler à ceci:

// 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 interval ID and call
  // clearInterval(), but we’d still want to do it in a single place.
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

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

Maintenant, les composants peuvent utiliser showNotificationWithTimeout sans dupliquer cette logique ou avoir des conditions de course avec des notifications différentes:

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

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

Pourquoi showNotificationWithTimeout() Acceptez dispatch comme le premier argument? Parce qu'il doit envoyer des actions au magasin. Normalement, un composant a accès à dispatch mais puisque nous voulons qu'une fonction externe prenne le contrôle de la répartition, nous devons lui donner le contrôle de l'envoi.

Si vous avez exporté un magasin singleton depuis un module, vous pouvez simplement l'importer et dispatch directement à la place:

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

Cela semble plus simple mais nous ne recommandons pas cette approche. La principale raison pour laquelle nous n'aimons pas c'est parce que il force le magasin à être un singleton. Cela rend très difficile à mettre en œuvre rendu du serveur. Sur le serveur, vous voulez que chaque requête ait son propre magasin, afin que les différents utilisateurs obtiennent des données préchargées différentes.

Un magasin singleton rend également les tests plus difficiles. Vous ne pouvez plus vous moquer d'un magasin lorsque vous testez des créateurs d'action car ils référencent un magasin réel spécifique exporté à partir d'un module spécifique. Vous ne pouvez même pas réinitialiser son état de l'extérieur.

Donc, alors que techniquement vous pouvez exporter un magasin singleton à partir d'un module, nous le déconseillons. Ne le faites pas à moins d'être sûr que votre application n'ajoutera jamais le rendu du serveur.

Revenir à la version précédente:

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

Cela résout les problèmes de duplication de la logique et nous évite les conditions de course.

Thunk Middleware

Pour les applications simples, l'approche devrait suffire. Ne vous inquiétez pas du middleware si vous êtes satisfait.

Dans les applications plus grandes, cependant, vous pourriez rencontrer certains inconvénients.

Par exemple, il semble regrettable que nous devions passer dispatch autour. Cela rend plus difficile à conteneur séparé et composants de présentation parce que tout composant qui distribue les actions Redux de manière asynchrone de la manière ci-dessus doit accepter dispatch comme un accessoire afin qu'il puisse le passer plus loin. Vous ne pouvez pas simplement lier les créateurs d'actions avec connect() plus parce que showNotificationWithTimeout() n'est pas vraiment un créateur d'action. Il ne retourne pas une action Redux.

En outre, il peut être gênant de se rappeler quelles fonctions sont les créateurs d'action synchrones comme showNotification() et qui sont des aides asynchrones comme showNotificationWithTimeout(). Vous devez les utiliser différemment et faites attention à ne pas les confondre.

C'était la motivation pour trouver un moyen de «légitimer» ce modèle de fourniture dispatch à une fonction d'aide, et aider Redux à "voir" ces créateurs d'action asynchrones comme un cas particulier de créateurs d'actions normales plutôt que des fonctions totalement différentes.

Si vous êtes toujours avec nous et que vous reconnaissez également un problème dans votre application, vous pouvez utiliser le Redux Thunk middleware.

Dans un sens, Redux Thunk enseigne à Redux de reconnaître des types spéciaux d'actions qui sont en fait des fonctions:

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

Lorsque ce middleware est activé, si vous envoyez une fonction, Middleware Redux Thunk le donnera dispatch comme argument. Il va également "avaler" de telles actions donc ne vous inquiétez pas si vos réducteurs reçoivent des arguments de fonction bizarres. Vos réducteurs recevront uniquement des actions d'objets simples, soit émises directement, soit émises par les fonctions que nous venons de décrire.

Cela ne semble pas très utile, n'est-ce pas? Pas dans cette situation particulière. Cependant, il nous permet de déclarer showNotificationWithTimeout() en tant que créateur d'actions Redux régulier:

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

Notez comment la fonction est presque identique à celle que nous avons écrite dans la section précédente. Cependant, il n'accepte pas dispatchcomme le premier argument. Au lieu de cela résultats une fonction qui accepte dispatch comme le premier argument.

Comment l'utiliserions-nous dans notre composant? Certainement, nous pourrions écrire ceci:

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

Nous appelons le créateur d'action asynchrone pour obtenir la fonction interne qui veut juste dispatchpuis on passe dispatch.

Cependant c'est encore plus gênant que la version originale! Pourquoi sommes-nous allés de cette façon?

A cause de ce que je t'ai déjà dit. Si le middleware Redux Thunk est activé, chaque fois que vous tentez d'envoyer une fonction à la place d'un objet d'action, le middleware appelle cette fonction avec dispatch méthode elle-même comme le premier argument.

Nous pouvons donc le faire à la place:

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

Enfin, l'envoi d'une action asynchrone (en réalité, une série d'actions) ne diffère pas de l'envoi d'une seule action de manière synchrone au composant. Ce qui est bien parce que les composants ne devraient pas se soucier si quelque chose se produit de manière synchrone ou asynchrone. Nous avons juste fait abstraction de cela.

Notez que puisque nous avons "enseigné" à Redux de reconnaître ces créateurs d'actions "spéciaux" (nous les appelons thunk créateurs d'action), nous pouvons maintenant les utiliser dans n'importe quel endroit où nous utiliserions des créateurs d'actions réguliers. Par exemple, nous pouvons les utiliser avec 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 à Thunks

Habituellement, vos réducteurs contiennent la logique métier pour déterminer l'état suivant. Cependant, les réducteurs ne sont activés qu'après l'envoi des actions. Que faire si vous avez un effet secondaire (comme l'appel d'une API) dans un créateur d'action thunk, et que vous voulez l'empêcher dans certaines conditions?

Sans utiliser le middleware thunk, vous feriez simplement cette vérification dans le composant:

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

Cependant, le but de l'extraction d'un créateur d'action était de centraliser cette logique répétitive à travers de nombreux composants. Heureusement, Redux Thunk vous offre un moyen de lis l'état actuel du magasin Redux. En plus de dispatch, ça passe aussi getState comme deuxième argument de la fonction, vous revenez de votre créateur d'action thunk. Cela permet au thunk de lire l'état actuel du magasin.

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

Ne pas abuser de ce modèle. Il est bon pour renoncer aux appels API quand il y a des données en cache disponibles, mais ce n'est pas une très bonne base pour construire votre logique métier. Si tu utilises getState() seulement pour répartir conditionnellement différentes actions, pensez plutôt à placer la logique métier dans les réducteurs.

Prochaines étapes

Maintenant que vous avez une intuition de base sur le fonctionnement des thunks, consultez Redux exemple asynchrone qui les utilise.

Vous pouvez trouver de nombreux exemples dans lesquels les thunks renvoient des promesses. Ce n'est pas obligatoire mais peut être très pratique. Redux ne se soucie pas de ce que vous revenez d'un thunk, mais il vous donne sa valeur de retour de dispatch(). C'est pourquoi vous pouvez renvoyer une promesse d'un thunk et l'attendre pour terminer en appelant dispatch(someThunkReturningPromise()).then(...).

Vous pouvez également diviser des créateurs d'action thunk complexes en plusieurs petits créateurs d'action thunk. le dispatch La méthode fournie par les thunks peut accepter les thunks eux-mêmes, donc vous pouvez appliquer le pattern de manière récursive. Encore une fois, cela fonctionne mieux avec Promises car vous pouvez implémenter un flux de contrôle asynchrone en plus de cela.

Pour certaines applications, vous pouvez vous trouver dans une situation où vos exigences de flux de contrôle asynchrone sont trop complexes pour être exprimées avec des thunk. Par exemple, réessayer des demandes ayant échoué, effectuer un flux de ré-autorisation avec des jetons ou un processus d'intégration étape par étape peut être trop verbeux et sujet aux erreurs lorsqu'il est écrit de cette façon. Dans ce cas, vous pouvez envisager des solutions de flux de contrôle asynchrones plus avancées, telles que Redux Saga ou Redux Loop. Evaluez-les, comparez les exemples correspondant à vos besoins et choisissez celui qui vous plaît le plus.

Enfin, n'utilisez rien (y compris les thunks) si vous n'en avez pas vraiment besoin. Rappelez-vous que, selon les exigences, votre solution peut sembler aussi simple que

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

Ne transpire pas à moins de savoir pourquoi tu fais ça.


1969
2018-02-15 17:33



Utiliser Redux-saga

Comme Dan Abramov l'a dit, si vous voulez un contrôle plus avancé de votre code asynchrone, vous pouvez jeter un oeil à redux-saga.

Cette réponse est un exemple simple, si vous voulez de meilleures explications sur pourquoi redux-saga peut être utile pour votre application, vérifiez cette autre réponse.

L'idée générale est que Redux-saga offre un interpréteur de générateurs ES6 qui vous permet d'écrire facilement du code asynchrone qui ressemble à du code synchrone (c'est pourquoi vous trouverez souvent des boucles infinies dans Redux-saga). D'une certaine manière, Redux-saga construit son propre langage directement dans Javascript. Redux-saga peut sembler un peu difficile à apprendre au début, parce que vous avez besoin d'une compréhension de base des générateurs, mais aussi de comprendre le langage offert par Redux-saga.

Je vais essayer ici de décrire ici le système de notification que j'ai construit sur redux-saga. Cet exemple est actuellement exécuté en production.

Spécification avancée du système de notification

  • Vous pouvez demander une notification à afficher
  • Vous pouvez demander une notification pour masquer
  • Une notification ne doit pas être affichée plus de 4 secondes
  • Plusieurs notifications peuvent être affichées en même temps
  • Pas plus de 3 notifications peuvent être affichées en même temps
  • Si une notification est demandée alors qu'il y a déjà 3 notifications affichées, alors mettez-la en file d'attente.

Résultat

Capture d'écran de mon application de production Stample.co

toasts

Code

Ici, j'ai nommé la notification un toast mais ceci est un détail de dénomination.

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

Et le réducteur:

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

Usage

Vous pouvez simplement envoyer TOAST_DISPLAY_REQUESTED événements. Si vous envoyez 4 demandes, seulement 3 notifications seront affichées, et la 4ème s'affichera un peu plus tard une fois que la 1ère notification aura disparu.

Notez que je ne recommande pas spécifiquement l'envoi TOAST_DISPLAY_REQUESTED de JSX. Vous préférez ajouter une autre saga qui écoute vos événements d'application déjà existants, puis envoyer le TOAST_DISPLAY_REQUESTED: votre composant qui déclenche la notification n'a pas besoin d'être étroitement couplé au système de notification.

Conclusion

Mon code n'est pas parfait mais fonctionne en production avec 0 bugs pendant des mois. Redux-saga et les générateurs sont un peu difficiles au départ, mais une fois que vous les avez compris, ce type de système est assez facile à construire.

Il est même assez facile d'implémenter des règles plus complexes, comme:

  • Lorsque trop de notifications sont "mises en file d'attente", réduisez le temps d'affichage pour chaque notification afin que la taille de la file d'attente diminue plus rapidement.
  • détecter les changements de taille de fenêtre et modifier le nombre maximal de notifications affichées en conséquence (par exemple, desktop = 3, phone portrait = 2, phone landscape = 1)

Honnêtement, bonne chance à mettre en œuvre ce genre de choses correctement avec thunks.

Notez que vous pouvez faire exactement le même genre de chose avec redux-observable ce qui est très similaire à redux-saga. C'est presque la même chose et c'est une question de goût entre les générateurs et RxJS.


142
2017-07-25 17:44



Vous pouvez le faire avec redux-thunk. Il y a un guide dans le document redux pour les actions asynchrones comme setTimeout.


18
2018-02-15 14:16



Je recommanderais également de jeter un coup d'oeil à Modèle SAM.

Le modèle SAM préconise l'inclusion d'un «prédicat de prochaine action» où les actions (automatiques) telles que «les notifications disparaissent automatiquement après 5 secondes» sont déclenchées une fois le modèle mis à jour (modèle SAM ~ état réducteur + magasin).

Le modèle préconise des actions de séquençage et des mutations de modèle une à la fois, parce que «l'état de contrôle» du modèle «contrôle» les actions qui sont activées et / ou exécutées automatiquement par le prédicat de la prochaine action. Vous ne pouvez pas prédire (en général) quel état sera le système avant de traiter une action et donc si votre prochaine action attendue sera permise / possible.

Ainsi, par exemple, le code,

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

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

ne serait pas autorisé avec SAM, car le fait qu'une action hideNotification puisse être distribuée dépend du fait que le modèle accepte avec succès la valeur "showNotication: true". Il pourrait y avoir d'autres parties du modèle qui l'empêchent de l'accepter et, par conséquent, il n'y aurait aucune raison de déclencher l'action hideNotification.

Je recommande fortement d'implémenter un prédicat approprié après la mise à jour du magasin et le nouvel état de contrôle du modèle peut être connu. C'est la façon la plus sûre d'implémenter le comportement que vous recherchez.

Vous pouvez nous rejoindre sur Gitter si vous le souhaitez. Il y a aussi SAM guide de démarrage disponible ici.


17
2018-02-24 02:31



Un référentiel avec des exemples de projets

Actuellement, il existe quatre exemples de projets:

  1. Écriture du code asynchrone en ligne
  2. Extraction du créateur d'action asynchrone
  3. Utiliser Redux Thunk
  4. Utiliser Redux Saga

La réponse acceptée est géniale.

Mais il manque quelque chose:

  1. Aucun exemple de projet exécutable, seulement quelques extraits de code.
  2. Aucun exemple de code pour d'autres alternatives, telles que:
    1. Redux Saga

J'ai donc créé le Bonjour Async référentiel pour ajouter les choses manquantes:

  1. Projets exécutables. Vous pouvez les télécharger et les exécuter sans modification.
  2. Fournir un exemple de code pour plus d'alternatives:

Redux Saga

La réponse acceptée fournit déjà des exemples d'extraits de code pour Async Code Inline, Async Action Generator et Redux Thunk. Par souci d'exhaustivité, je fournis des extraits de code pour 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 }
}

Les actions sont simples et pures.

// component.js

import { connect } from 'react-redux'

// ...

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

// ...

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

Rien n'est spécial avec le composant.

// 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 sont basées sur Générateurs ES6

// index.js

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

const sagaMiddleware = createSagaMiddleware()

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

sagaMiddleware.run(saga)

Comparé à Redux Thunk

Avantages

  • Vous ne finissez pas dans l'enfer de rappel.
  • Vous pouvez tester vos flux asynchrones facilement.
  • Vos actions restent pures.

Les inconvénients

  • Cela dépend des générateurs ES6 qui sont relativement nouveaux.

Veuillez vous référer au projet runnable si les extraits de code ci-dessus ne répondent pas à toutes vos questions.


16
2017-12-24 12:53



Après avoir essayé les différentes approches populaires (créateurs d'action, thunks, sagas, épopées, effets, middleware personnalisés), je sentais encore qu'il y avait peut-être place à amélioration alors j'ai documenté mon parcours dans cet article de blog, Où dois-je mettre ma logique métier dans une application React / Redux? 

Tout comme les discussions ici, j'ai essayé de comparer et de comparer les différentes approches. Finalement, cela m'a amené à introduire une nouvelle bibliothèque redux-logic qui s'inspire des épopées, des sagas, du middleware personnalisé.

Il vous permet d'intercepter des actions pour valider, vérifier, autoriser, ainsi que de fournir un moyen d'effectuer des opérations d'E / S asynchrones.

Certaines fonctionnalités courantes peuvent simplement être déclarées comme l'annulation, la limitation, l'annulation et l'utilisation de la réponse de la dernière requête (takeLatest). redux-logic enveloppe votre code en fournissant cette fonctionnalité pour vous.

Cela vous libère de mettre en œuvre votre cœur de métier comme vous le souhaitez. Vous n'êtes pas obligé d'utiliser des observables ou des générateurs, sauf si vous le souhaitez. Utiliser les fonctions et les callbacks, les promesses, les fonctions asynchrones (async / await), etc.

Le code pour faire une notification 5s simple serait quelque chose comme:

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

J'ai un exemple de notification plus avancé dans mon repo qui fonctionne de manière similaire à ce que Sebastian Lorber a décrit, où vous pouvez limiter l'affichage à N éléments et faire pivoter ceux qui sont en file d'attente. Exemple de notification redux-logic 

J'ai une variété de redux-logic jsfiddle exemples en direct ainsi que des exemples complets. Je continue à travailler sur des docs et des exemples.

J'aimerais entendre vos commentaires.


13
2017-08-25 01:15



Si vous souhaitez gérer un délai d'attente sur des actions sélectives, vous pouvez essayer middleware approche. J'ai fait face à un problème similaire pour gérer des actions basées sur des promesses de manière sélective et cette solution était plus flexible.

Disons que votre créateur d'action ressemble à ceci:

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

timeout peut contenir plusieurs valeurs dans l'action ci-dessus

  • nombre en ms - pour une durée de timeout spécifique
  • true - pour une durée d'expiration constante. (manipulé dans le middleware)
  • undefined - pour une expédition immédiate

Votre implémentation de middleware ressemblerait à ceci:

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

Vous pouvez maintenant router toutes vos actions à travers cette couche de middleware en utilisant redux.

createStore(reducer, applyMiddleware(timeoutMiddleware))

Vous pouvez trouver des exemples similaires ici


5
2017-09-15 13:24



Je comprends que cette question est un peu ancienne mais je vais présenter une autre solution en utilisant redux-observable alias. Épique.

Citant la documentation officielle:

Qu'est-ce que redux-observable?

RxJS 5-middleware pour Redux. Composer et annuler les actions asynchrones   créer des effets secondaires et plus encore.

Une épopée est la primitive de base de redux-observable.

C'est une fonction qui prend un flux d'actions et renvoie un flux   d'actions. Actions dans, actions sur.

En plus ou en moins de mots, vous pouvez créer une fonction qui reçoit des actions via un flux, puis renvoyer un nouveau flux d'actions (en utilisant des effets secondaires communs tels que les délais, les délais, les intervalles et les demandes).

Permettez-moi de poster le code et d'en expliquer un peu plus à ce sujet

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)

Le code clé pour résoudre ce problème est aussi simple que le tarte que vous pouvez voir, la seule chose qui semble différente des autres réponses est la fonction rootEpic.

Point 1. Comme pour les sagas, vous devez combiner les épopées afin d'obtenir une fonction de haut niveau qui reçoit un flux d'actions et retourne un flux d'actions, de sorte que vous pouvez l'utiliser avec l'usine middleware createEpicMiddleware. Dans notre cas, nous avons seulement besoin d'un seul, donc nous avons seulement notre rootEpic Nous n'avons donc pas besoin de combiner quoi que ce soit, mais c'est bon à savoir.

Point 2. Notre rootEpic qui prend soin de la logique des effets secondaires ne prend que 5 lignes de code ce qui est génial! Y compris le fait que c'est à peu près déclaratif!

Point 3. Ligne par ligne explication rootEpic (dans les commentaires)

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

J'espère que ça aide!


5
2018-04-28 02:52