web-dev-qa-db-fra.com

Comment envoyer une action Redux avec un délai d'attente?

J'ai une action qui met à jour l'état de notification de mon application. Habituellement, cette notification sera une erreur ou une information quelconque. Je dois ensuite envoyer une autre action au bout de 5 secondes pour rétablir l'état de notification initial, donc aucune notification. La principale raison derrière cela est de fournir une fonctionnalité permettant aux notifications de disparaître automatiquement après 5 secondes.

Je n'ai eu aucune chance d'utiliser setTimeout et de renvoyer une autre action, mais je ne trouve pas comment cela se fait en ligne. Donc, tout conseil est le bienvenu.

793
Ilja

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

Redux ne offre quelques moyens alternatifs de gérer les fichiers asynchrones, mais vous ne devriez les utiliser que lorsque vous réalisez que vous répétez trop de code. Sauf si vous avez ce problème, utilisez ce que le langage propose et optez pour la solution la plus simple.

Écriture de code asynchrone en ligne

C'est de loin le moyen le plus simple. Et il n’ya 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, mais vous obtenez soit dispatch(), soit 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 de fautes de frappe lors de la distribution des mêmes actions à partir de différents composants, vous pouvez extraire les créateurs d'action 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 des cas simples, mais vous pouvez constater qu'elle présente quelques problèmes:

  • Cela vous oblige à dupliquer cette logique partout où vous souhaitez afficher une notification.
  • Les notifications n’ayant pas d’ID, vous aurez une situation de concurrence critique si vous affichez deux notifications assez rapidement. Une fois le premier délai écoulé, il enverra HIDE_NOTIFICATION, masquant par erreur la deuxième notification plus tôt qu'après le délai.

Pour résoudre ces problèmes, vous devez extraire une fonction qui centralise la logique de temporisation 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 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)
}

Désormais, les composants peuvent utiliser showNotificationWithTimeout sans dupliquer cette logique ni avoir des conditions de concurrence 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() accepte-t-il dispatch comme premier argument? Parce qu'il faut envoyer des actions au magasin. Normalement, un composant a accès à dispatch, mais comme nous voulons qu'une fonction externe prenne le contrôle de la répartition, nous devons lui donner le contrôle de la répartition.

Si vous aviez un magasin singleton exporté depuis un module, vous pouvez simplement l'importer et dispatch directement dessus:

// 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 cela est que oblige store à être un singleton . Cela rend très difficile à mettre en œuvre rendu du serveur . Sur le serveur, vous souhaiterez que chaque demande ait son propre magasin, afin que 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 simuler un magasin lors du test des créateurs d'action car ils font référence à un magasin réel spécifique exporté à partir d'un module spécifique. Vous ne pouvez même pas réinitialiser son état depuis l’extérieur.

Ainsi, bien que vous puissiez techniquement exporter un singleton store à partir d’un module, nous le décourageons. Ne le faites pas sauf si vous êtes sûr que votre application n’ajoutera jamais de rendu de 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 des conditions de concurrence.

Middleware Thunk

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

Toutefois, dans les applications plus grandes, certains désagréments peuvent être gênants.

Par exemple, il semble malheureux que nous devions passer dispatch. Cela rend plus délicat de composants de conteneur et de présentation séparés car tout composant qui distribue les actions Redux de manière asynchrone de la manière décrite ci-dessus doit accepter dispatch comme support pour pouvoir le transmettre. Vous ne pouvez plus simplement lier les créateurs d’action avec connect(), car showNotificationWithTimeout() n’est pas vraiment un créateur d’action. Cela ne retourne pas d'action Redux.

En outre, il peut être délicat de se souvenir des fonctions qui sont des créateurs d’action synchrones comme showNotification() et des aides asynchrones comme showNotificationWithTimeout(). Vous devez les utiliser différemment et veillez à ne pas les confondre.

C’est la motivation qui a poussé à trouver un moyen de "légitimer" ce type de fourniture de dispatch à une fonction d’aide, et d’aider Redux à "voir", tels que les créateurs d’actions asynchrones, créateurs d'actions normales plutôt que de 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 middleware Redux Thunk .

Dans un Gist, Redux Thunk enseigne à Redux à reconnaître des types particuliers d’actions qui sont en réalité 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 , le middleware Redux Thunk lui donnera dispatch comme argument. Cela "avalera" de telles actions, ne craignez donc pas que vos réducteurs reçoivent des arguments de fonction étranges. Vos réducteurs ne recevront que des actions simples, soit émises directement, soit émises par les fonctions décrites précédemment.

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

// 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 que la fonction est presque identique à celle décrite dans la section précédente. Cependant, il n’accepte pas dispatch comme premier argument. À la place, il renvoie une fonction qui accepte dispatch comme 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 dispatch, puis nous passons dispatch.

Cependant, cela est encore plus délicat que la version originale! Pourquoi avons-nous même aller 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 de distribuer une fonction au lieu d'un objet d'action, le middleware appelle cette fonction avec la méthode dispatch elle-même comme premier argument .

Nous pouvons donc faire cela à 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 car les composants ne doivent pas se soucier de savoir si quelque chose se passe de manière synchrone ou asynchrone. Nous venons de résumer cela.

Notez que depuis que nous avons "appris" à Redux à reconnaître de tels créateurs d’actions "spéciales" (nous les appelons thunk créateurs d’actions), nous pouvons maintenant les utiliser à n’importe quel endroit où nous utiliserions des créateurs d’actions ordinaires. 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)

Etat de lecture à Thunks

Généralement, vos réducteurs contiennent la logique métier permettant de déterminer le prochain état. Cependant, les réducteurs n'interviennent qu'après l'envoi des actions. Que se passe-t-il si vous avez un effet secondaire (tel que l'appel d'une API) chez un créateur d'actions thunk et que vous souhaitez l'éviter sous certaines conditions?

Sans utiliser le middleware thunk, vous feriez simplement cette vérification à l'intérieur du 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 sur de nombreux composants. Heureusement, Redux Thunk vous offre un moyen de lire l’état actuel du magasin Redux. En plus de dispatch, il transmet également getState en tant que deuxième argument de la fonction renvoyée par le créateur de votre 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 de supprimer les appels d'API lorsque des données en cache sont disponibles, mais ce n'est pas une très bonne base sur laquelle construire votre logique métier. Si vous utilisez getState() uniquement pour envoyer de manière conditionnelle différentes actions, envisagez de placer la logique applicative 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 des thunks renvoient des promesses. Ce n'est pas obligatoire mais peut être très pratique. Redux ne se soucie pas de ce que vous retournez d’un thunk, mais il vous donne sa valeur de retour de dispatch(). C'est pourquoi vous pouvez retourner une promesse d'un thunk et attendre qu'elle se termine en appelant dispatch(someThunkReturningPromise()).then(...).

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

Pour certaines applications, vous pouvez vous trouver dans une situation où vos exigences en matière de flux de contrôle asynchrone sont trop complexes pour être exprimées avec Thunks. Par exemple, réessayer des demandes ayant échoué, un flux de réautorisations avec des jetons ou une intégration pas à pas peut être trop prolixe et source d'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 . Évaluez-les, comparez les exemples correspondant à vos besoins et choisissez celui que vous préférez.

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

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

Ne vous en faites pas, à moins que vous sachiez pourquoi vous le faites.

2407
Dan Abramov

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 souhaitez mieux expliquer pourquoi redux-saga peut être utile pour votre application, cochez 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 ressemblant à du code synchrone (c’est pourquoi vous trouverez souvent des boucles while infinies dans Redux-saga). Redux-saga est en train de construire son propre langage directement dans Javascript. Redux-saga peut sembler un peu difficile à apprendre au début, car il vous faut une compréhension de base des générateurs, mais vous devez également comprendre le langage proposé par Redux-saga.

Je vais essayer ici de décrire le système de notification que j’ai construit sur Redux-Saga. Cet exemple est actuellement en production.

Système de notification avancé

  • Vous pouvez demander qu'une notification soit affichée
  • Vous pouvez demander une notification pour cacher
  • 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 que 3 notifications sont déjà affichées, mettez-les en file d'attente/différées.

Résultat

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

toasts

Code

Ici, j'ai nommé la notification toast mais il s'agit d'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 des événements TOAST_DISPLAY_REQUESTED. Si vous envoyez 4 demandes, seules 3 notifications seront affichées et la 4ème apparaîtra un peu plus tard une fois que la 1ère notification aura disparu.

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

Conclusion

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

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

  • lorsque trop de notifications sont "mises en file d'attente", accordez moins de temps d'affichage pour chaque notification afin que la taille de la file d'attente puisse diminuer plus rapidement.
  • détecter les modifications de la taille de la fenêtre et modifier le nombre maximal de notifications affichées en conséquence (par exemple, bureau = 3, portrait du téléphone = 2, paysage du téléphone = 1)

Honnêtement, bonne chance pour implémenter ce genre de choses correctement avec Thunks.

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

170
Sebastien Lorber

n référentiel avec des exemples de projets

Actuellement, il y a quatre exemples de projets:

  1. écriture de code asynchrone en ligne
  2. créateur de l'action asynchrone
  3. tiliser Redux Thunk
  4. tilisez Redux Saga

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

Mais il manque quelque chose:

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

J'ai donc créé le référentiel Hello Async pour ajouter les éléments manquants:

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

Redux Saga

La réponse acceptée fournit déjà des exemples de fragments 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 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

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

Par rapport à Redux Thunk

Avantages

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

Les inconvénients

  • Cela dépend des générateurs ES6, qui est relativement nouveau.

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

21
Tyler Long

Je recommanderais également de jeter un oeil sur le modèle SAM .

Le modèle SAM préconise l'inclusion d'un "prédicat d'action suivante" dans lequel des actions (automatiques) telles que "les notifications disparaissent automatiquement au bout de 5 secondes" sont déclenchées une fois le modèle mis à jour (modèle SAM ~ réducteur état + magasin).

Le modèle préconise de séquencer les actions et les mutations de modèle, une par une, car "l'état de contrôle" du modèle "contrôle" les actions activées et/ou automatiquement exécutées par le prédicat d'action suivante. Vous ne pouvez tout simplement pas prédire (en général) l'état du système avant le traitement d'une action et par conséquent si votre prochaine action attendue sera autorisée/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 répartie dépend du modèle acceptant avec succès la valeur "showNotication: true". D'autres parties du modèle pourraient l'empêcher de l'accepter et, par conséquent, il n'y aurait aucune raison de déclencher l'action hideNotification.

Je recommanderais fortement de mettre en place un prédicat approprié pour l'action suivante après les mises à jour du magasin et le nouvel état de contrôle du modèle. C'est le moyen le plus sûr de mettre en œuvre le comportement que vous recherchez.

Vous pouvez nous rejoindre sur Gitter si vous le souhaitez. Il existe également un guide de démarrage SAM disponible ici .

19
Jean-Jacques Dubray

Vous pouvez le faire avec redux-thunk . Il existe un guide dans le document redux pour les actions asynchrones telles que setTimeout.

19
Fatih Erikli

Après avoir essayé diverses approches populaires (créateurs d’actions, thunks, sagas, épopées, effets, middleware personnalisé), j’ai toujours eu l’impression que des améliorations pouvaient être apportées, j’ai donc documenté mon parcours dans cet article de blog Où est-ce que je place ma logique métier dans une application React/Redux?

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

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

Certaines fonctionnalités courantes peuvent simplement être déclarées, telles que le rebond, la limitation, l’annulation et l’utilisation exclusive de la réponse de la dernière demande (takeLatest). redux-logic enveloppe votre code en vous offrant cette fonctionnalité.

Cela vous permet de mettre en œuvre votre logique métier principale comme vous le souhaitez. Vous n'êtes pas obligé d'utiliser des observables ou des générateurs sauf si vous le souhaitez. Utiliser des fonctions et des rappels, des promesses, des fonctions asynchrones (async/wait), etc.

Le code permettant de faire une simple notification 5s 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 référentiel qui fonctionne de manière similaire à celle décrite par Sebastian Lorber: 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é d'exemples vivants de redux-logic jsfiddle ainsi que d'exemples complets . Je continue à travailler sur des documents et des exemples.

J'aimerais entendre vos commentaires.

17
Jeff Barczewski

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

Citation de la documentation officielle:

Qu'est-ce que redux-observable?

Middleware basé sur RxJS 5 pour Redux. Composez et annulez les actions asynchrones pour créer des effets secondaires, etc.

Une épopée est le noyau primitif de redux-observable.

C'est une fonction qui prend un flux d'actions et retourne un flux d'actions. Actions in, actions out.

En plus ou 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 courants tels que des délais d'attente, des délais, des intervalles et des requêtes).

Laissez-moi poster le code puis expliquez-en 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 vous pouvez le constater, la seule chose qui semble différente des autres réponses est la fonction rootEpic.

Point 1. Comme avec les sagas, vous devez combiner les épopées pour obtenir une fonction de niveau supérieur recevant un flux d’actions et renvoyant un flux d’actions, afin que vous puissiez l’utiliser avec la fabrique de middleware createEpicMiddleware . Dans notre cas, nous n’avons besoin que d’un seul, nous n'avons donc que notre rootEpic , nous n’avons donc pas à combiner quoi que ce soit, mais c’est une bonne chose à savoir.

Point 2. Notre rootEpic qui s'occupe 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 assez déclaratif!

Point 3. Explication de rootEpic ligne par ligne (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!

7
cnexans

Pourquoi cela devrait-il être si difficile? C'est juste la logique de l'interface utilisateur. Utilisez une action dédiée pour définir les données de notification:

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

et un composant dédié pour l'afficher:

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

Dans ce cas, les questions doivent être "comment nettoyer l'ancien état?", "Comment notifier un composant que l'heure a changé"

Vous pouvez implémenter une action TIMEOUT qui est répartie sur setTimeout à partir d'un composant.

Peut-être qu'il est juste de le nettoyer chaque fois qu'une nouvelle notification est affichée.

Quoi qu'il en soit, il devrait y avoir quelque setTimeout quelque part, non? Pourquoi ne pas le faire dans un composant

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

La motivation est que la fonctionnalité "fondu de notification" est vraiment une préoccupation de l'interface utilisateur. Cela simplifie donc les tests pour votre logique métier.

Il ne semble pas logique de tester sa mise en œuvre. Cela n'a de sens que de vérifier quand la notification doit expirer. Donc moins de code à stub, des tests plus rapides, un code plus propre.

5
Vanuan

Si vous souhaitez un délai d'attente pour certaines actions, vous pouvez essayer l'approche middleware . J'ai rencontré un problème similaire pour gérer de manière sélective les actions basées sur les promesses 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 temporisation spécifique
  • true - pour une durée de temporisation constante. (manipulé dans le middleware)
  • non défini - pour 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
Yash

La manière appropriée de le faire est d'utiliser Redux Thunk, un middleware populaire pour Redux, selon la documentation de Redux Thunk:

"Le middleware Redux Thunk vous permet d’écrire des créateurs d’action qui renvoient une fonction au lieu d’une action. Le thunk peut être utilisé pour retarder l’envoi d’une action ou uniquement si une certaine condition est remplie. La fonction interne reçoit les méthodes de stockage. dispatch et getState en tant que paramètres ".

Donc, fondamentalement, il renvoie une fonction et vous pouvez retarder votre envoi ou le mettre dans un état.

Donc, quelque chose comme ça va faire le travail pour vous:

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

C'est simple. Utilisez le paquet trim-redux et écrivez comme ceci dans componentDidMount ou ailleurs et tuez-le dans componentWillUnmount.

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

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

Redux lui-même est une jolie bibliothèque verbeuse, et pour ce genre de choses, vous devrez utiliser quelque chose comme Redux-thunk , ce qui donnera une fonction dispatch, vous pourrez donc envoyer la fermeture de la notification après plusieurs secondes.

J'ai créé une bibliothèque pour résoudre des problèmes tels que la verbosité et la composabilité, et votre exemple ressemblera à ceci:

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

Nous composons donc des actions de synchronisation pour afficher les notifications à l'intérieur d'une action asynchrone, qui peut demander des informations sur l'arrière-plan, ou vérifier ultérieurement si la notification a été fermée manuellement.

1
Bloomca