Comment envoyer une action Redux avec un timeout?

891

J'ai une action qui met à jour l'état de notification de ma demande. Habituellement, cette notification sera une erreur ou une information quelconque. Je dois ensuite envoyer une autre action après 5 secondes qui ramènera l'état de notification à celui initial, donc pas de notification. La principale raison derrière cela est de fournir des fonctionnalités où les notifications disparaissent automatiquement après 5 secondes.

Je n'ai eu aucune chance d'utiliser setTimeoutet de retourner une autre action et je ne trouve pas comment cela se fait en ligne. Tout conseil est donc le bienvenu.

Ilja
la source
30
N'oubliez pas de vérifier ma redux-sagaréponse basée si vous voulez quelque chose de mieux que les thunks. Réponse tardive, vous devez donc faire défiler longtemps avant de voir apparaître :) ne signifie pas que cela ne vaut pas la peine d'être lu. Voici un raccourci: stackoverflow.com/a/38574266/82609
Sebastien Lorber
5
Chaque fois que vous définissez TimeTime, n'oubliez pas d'effacer la minuterie à l'aide de ClearTimeout dans la méthode du cycle de vie ComponentWillUnMount
Hemadri Dasari
2
redux-saga est cool mais ils ne semblent pas avoir de support pour les réponses typées des fonctions du générateur. Peu importe si vous utilisez tapuscrit avec react.
Crhistian Ramirez

Réponses:

2619

Ne tombez pas dans le piège de penser qu'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 n'offre des autres moyens de traiter avec des choses asynchrones, mais vous ne devez utiliser ceux lorsque vous réalisez que vous répétez trop de code. Sauf si vous rencontrez ce problème, utilisez ce que la langue 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'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 dispatch()des créateurs d'actions spécifiques ou spécifiques. Mais cela ne fait aucune différence pour nous.

Si vous n'aimez pas créer des fautes de frappe lors de la répartition des mêmes actions à partir de différents composants, vous souhaiterez peut-être extraire les créateurs d'actions au lieu de répartir les 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 préalablement 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 d'Async Action Creator

L'approche ci-dessus fonctionne très bien dans les cas simples, mais vous constaterez peut-être qu'elle présente quelques problèmes:

  • Cela vous oblige à dupliquer cette logique partout où vous souhaitez 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 d'expiration se termine, il sera envoyé HIDE_NOTIFICATION, masquant par erreur la deuxième notification plus tôt qu'après le délai d'expiration.

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 showNotificationWithTimeoutsans dupliquer cette logique ou avoir des conditions de concurrence avec différentes notifications:

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

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

Pourquoi showNotificationWithTimeout()accepte-t-on dispatchcomme premier argument? Parce qu'il doit envoyer des actions au magasin. Normalement, un composant a accès dispatchmais 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é à partir d'un module, vous pouvez simplement l'importer dispatchdirectement 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 ne l'aimons pas est qu'il force le magasin à être un singleton . Cela rend très difficile l'implémentation du rendu du serveur . Sur le serveur, vous souhaiterez que chaque demande ait son propre magasin, de sorte 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 vous moquer d'un magasin lors du test des créateurs d'actions, 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 de l'extérieur.

Donc, bien que vous puissiez techniquement exporter un magasin singleton à partir d'un module, nous le déconseillons. Ne faites cela que 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 les conditions de course.

Thunk Middleware

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

Cependant, dans les applications plus grandes, vous pouvez rencontrer certains inconvénients.

Par exemple, il semble regrettable que nous devions faire le dispatchtour. Il est donc plus difficile de séparer les conteneurs et les composants de présentation, car tout composant qui distribue des actions Redux de manière asynchrone de la manière ci-dessus doit accepter dispatchcomme accessoire pour pouvoir le transmettre plus loin. Vous ne pouvez plus simplement lier des créateurs d'action connect()car ce showNotificationWithTimeout()n'est pas vraiment un créateur d'action. Il ne renvoie pas d'action Redux.

De plus, il peut être difficile de se souvenir des fonctions comme les créateurs d'actions synchrones showNotification()et celles des assistants asynchrones showNotificationWithTimeout(). Vous devez les utiliser différemment et veillez à ne pas les confondre.

C'était la motivation pour trouver un moyen de «légitimer» ce modèle de fourniture dispatchd'une fonction d'aide, et aider Redux à «voir» ces créateurs d'actions asynchrones comme un cas spécial 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 middleware Redux Thunk .

Dans un sens, Redux Thunk enseigne à Redux à 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 distribuez une fonction , le middleware Redux Thunk la donnera dispatchen argument. Il "avalera" également de telles actions, alors ne vous inquiétez pas si vos réducteurs reçoivent des arguments de fonction étranges. Vos réducteurs ne recevront que des actions d'objets simples, soit émises directement, soit émises par les fonctions comme nous venons de le décrire.

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

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

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

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

Notez que la fonction est presque identique à celle que nous avons écrite dans la section précédente. Cependant, il n'accepte pas dispatchcomme premier argument. Au lieu de cela, il renvoie une fonction qui accepte dispatchcomme 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 intérieure qui veut juste dispatch, puis nous passons dispatch.

Cependant, c'est encore plus gênant que la version originale! Pourquoi avons-nous même choisi cette voie?

À cause de ce que je vous ai dit auparavant. Si le middleware Redux Thunk est activé, chaque fois que vous essayez de distribuer une fonction au lieu d'un objet action, le middleware appellera cette fonction avec la dispatchméthode elle-même comme premier argument .

Nous pouvons donc le faire à la place:

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

Enfin, la répartition d'une action asynchrone (en réalité, une série d'actions) ne ressemble pas à la répartition d'une seule action de manière synchrone sur le composant. Ce qui est bien car les composants ne devraient pas se soucier de savoir si quelque chose se produit de manière synchrone ou asynchrone. Nous avons simplement résumé cela.

Notez que depuis que nous avons « appris » Redux reconnaître ces créateurs d'action « spéciaux » (nous les appelons thunk créateurs d'action), nous pouvons maintenant les utiliser dans un endroit où nous utiliserions les créateurs d'action 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)

État de lecture dans Thunks

Habituellement, vos réducteurs contiennent la logique métier pour déterminer l'état suivant. Cependant, les réducteurs n'interviennent qu'après l'envoi des actions. Que se passe-t-il si vous avez un effet secondaire (comme appeler une API) dans un créateur d'action thunk, et que vous souhaitez l'empêcher dans certaines conditions?

Sans utiliser le middleware thunk, vous devez simplement effectuer 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 d'extraire 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 de la boutique Redux. De plus dispatch, il passe également getStatecomme deuxième argument à la fonction que vous renvoyez 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)
  }
}

N'abusez pas de ce modèle. C'est bon pour échapper aux appels d'API lorsqu'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 vous utilisez getState()uniquement pour répartir conditionnellement différentes actions, envisagez plutôt de 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 l' exemple asynchrone Redux qui les utilise.

Vous pouvez trouver de nombreux exemples dans lesquels les thunks retournent 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 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 les créateurs d'actions de thunk complexes en plusieurs créateurs d'action de thunk plus petits. La dispatchméthode fournie par thunks peut accepter les thunks elle-même, vous pouvez donc appliquer le modèle 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 retrouver dans une situation où vos exigences de flux de contrôle asynchrone sont trop complexes pour être exprimées avec des thunks. Par exemple, une nouvelle tentative de demandes ayant échoué, un flux de réautorisation avec des jetons ou une intégration étape par étape peuvent être trop verbeux et sujets aux erreurs lorsqu'ils sont écrits de cette façon. Dans ce cas, vous souhaiterez peut-être examiner des solutions de flux de contrôle asynchrones plus avancées telles que Redux Saga ou Redux Loop . Évaluez-les, comparez les exemples pertinents à vos besoins et choisissez celui que vous aimez le plus.

Enfin, n'utilisez rien (y compris les thunks) si vous n'en avez pas vraiment besoin. N'oubliez pas 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 le transpirez que si vous savez pourquoi vous faites cela.

Dan Abramov
la source
27
Les actions asynchrones semblent être une solution si simple et élégante à un problème commun. Pourquoi leur support n'est-il pas intégré à redux sans avoir besoin de middleware? Cette réponse pourrait alors être beaucoup plus concise.
Phil Mander
83
@PhilMander Parce qu'il existe de nombreux modèles alternatifs comme github.com/raisemarketplace/redux-loop ou github.com/yelouafi/redux-saga qui sont tout aussi (sinon plus) élégants. Redux est un outil de bas niveau. Vous pouvez créer un sur-ensemble que vous aimez et le distribuer séparément.
Dan Abramov
16
Pouvez-vous expliquer ceci: * envisagez de mettre la logique métier dans les réducteurs *, cela signifie-t-il que je devrais envoyer une action, puis déterminer dans le réducteur quelles autres actions envoyer en fonction de mon état? Ma question est, puis-je envoyer d'autres actions directement dans mon réducteur, et sinon, d'où dois-je les envoyer?
froginvasion
25
Cette phrase ne s'applique qu'au cas synchrone. Par exemple, si vous écrivez, if (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' })vous devriez peut-être juste dispatch({ type: 'C', something: cond })choisir d'ignorer l'action dans les réducteurs en fonction de action.somethingl'état actuel.
Dan Abramov
29
@DanAbramov Vous avez obtenu mon vote positif juste pour cela "Sauf si vous avez ce problème, utilisez ce que la langue offre et optez pour la solution la plus simple." Ce n'est qu'après que j'ai réalisé qui l'avait écrit!
Matt Lacey
189

Utiliser Redux-saga

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

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

L'idée générale est que Redux-saga propose 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 while infinies dans Redux-saga). D'une certaine manière, Redux-saga construit son propre langage directement à l'intérieur de Javascript. Redux-saga peut sembler un peu difficile à apprendre au début, car vous avez besoin d'une compréhension de base des générateurs, mais aussi de comprendre le langage proposé par Redux-saga.

Je vais essayer ici de décrire ici le système de notification que j'ai construit au-dessus de redux-saga. Cet exemple fonctionne actuellement en production.

Spécification du système de notification avancé

  • Vous pouvez demander qu'une notification soit affichée
  • Vous pouvez demander une notification pour masquer
  • Une notification ne doit pas s'afficher 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, mettez-la en file d'attente / reportez-la.

Résultat

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

toasts

Code

Ici, j'ai nommé la notification a toastmais c'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 des TOAST_DISPLAY_REQUESTEDévénements. Si vous envoyez 4 demandes, seules 3 notifications seront affichées, et la 4ème apparaîtra un peu plus tard une fois la 1ère notification disparue.

Notez que je ne recommande pas spécifiquement l'envoi TOAST_DISPLAY_REQUESTEDdepuis JSX. Vous préférez ajouter une autre saga qui écoute vos événements d'application déjà existants, puis distribuer 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 tourne en production avec 0 bogue pendant des mois. Redux-saga et les générateurs sont un peu difficiles au début, mais une fois que vous les comprenez, 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", donnez moins de temps d'affichage pour chaque notification afin que la taille de la file d'attente puisse diminuer 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, 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 des 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 la même chose et c'est une question de goût entre les générateurs et RxJS.

Sébastien Lorber
la source
18
Je souhaite que votre réponse soit entrée plus tôt lorsque la question a été posée, car je ne peux pas être plus d'accord avec l'utilisation de la bibliothèque d'effets secondaires Saga pour une logique métier comme celle-ci. Les réducteurs et les créateurs d'actions sont destinés aux transitions d'état. Les workflows ne sont pas identiques aux fonctions de transition d'état. Les workflows traversent les transitions, mais ne sont pas des transitions elles-mêmes. Redux + React n'en a pas lui-même - c'est exactement pourquoi Redux Saga est si utile.
Atticus du
4
Merci, j'essaie de faire de mon mieux pour rendre la redux-saga populaire pour ces raisons :) trop peu de gens pensent qu'actuellement la redux-saga n'est qu'un remplacement pour les thunks et ne voient pas comment la redux-saga permet des workflows complexes et découplés
Sébastien Lorber
1
Exactement. Les actions et les réducteurs font tous partie de la machine d'état. Parfois, pour des workflows complexes, vous avez besoin d'autre chose pour orchestrer la machine d'état qui ne fait pas directement partie de la machine d'état elle-même!
Atticus
2
Actions: charges utiles / événements à l'état de transition. Réducteurs: fonctions de transition d'état. Composants: interfaces utilisateur reflétant l'état. Mais il manque un élément majeur - comment gérez-vous le processus de nombreuses transitions qui ont toutes leur propre logique qui déterminent la transition à effectuer ensuite? Redux Saga!
Atticus
2
@mrbrdo si vous lisez attentivement ma réponse, vous remarquerez que les délais de notification sont réellement traités avec yield call(delay,timeoutValue);: ce n'est pas la même API mais elle a le même effet
Sebastien Lorber
25

Un référentiel avec des exemples de projets

Actuellement, il existe quatre exemples de projets:

  1. Écriture de code asynchrone en ligne
  2. Extraction d'Async Action Creator
  3. Utilisez Redux Thunk
  4. Utilisez Redux Saga

La réponse acceptée est impressionnante.

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 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 de 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

Les Sagas sont basées sur des 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 vous retrouvez pas en enfer de rappel.
  • Vous pouvez tester facilement 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 au projet exécutable si les extraits de code ci-dessus ne répondent pas à toutes vos questions.

Tyler Long
la source
23

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

Fatih Erikli
la source
Juste une question de suivi rapide, lorsque vous utilisez un middleware, voici applyMiddleware(ReduxPromise, thunk)(createStore)comment vous ajoutez plusieurs middleware (séparés par un coma?) Car je n'arrive pas à faire fonctionner le thunk.
Ilja
1
@Ilja Cela devrait fonctionner:const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
geniuscarrier
22

Je recommanderais également de jeter un œil au modèle SAM .

Le modèle SAM préconise d'inclure un "prédicat d'action suivante" où des actions (automatiques) telles que "les notifications disparaissent automatiquement après 5 secondes" sont déclenchées une fois que le modèle a été mis à jour (modèle SAM ~ état réducteur + magasin).

Le modèle préconise de séquencer les actions et les mutations du modèle une à la fois, car "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 l'action suivante. Vous ne pouvez tout simplement 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 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 envoyé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 recommanderais fortement d'implémenter un prédicat d'action suivante approprié après les mises à jour du magasin et le nouvel état de contrôle du modèle peut être connu. 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. Un guide de démarrage SAM est également disponible ici .

Jean-Jacques Dubray
la source
Jusqu'à présent, je n'ai gratté la surface, mais je suis déjà ravi par le motif SAM. V = S( vm( M.present( A(data) ) ), nap(M))est tout simplement magnifique. Merci d'avoir partagé vos pensées et votre expérience. Je vais creuser plus profondément.
@ftor, merci! quand je l'ai écrit la première fois, j'avais le même sentiment. J'utilise SAM en production depuis près d'un an maintenant, et je ne peux pas penser à un moment où j'ai senti que j'avais besoin d'une bibliothèque pour implémenter SAM (même vdom, bien que je puisse voir quand il pourrait être utilisé). Une seule ligne de code, c'est tout! SAM produit du code isomorphe, il n'y a aucune ambiguïté sur la façon de gérer les appels asynchrones ... Je ne peux pas penser à un moment où je pense, que fais-je?
métaprogrammeur
SAM est un véritable modèle d'ingénierie logicielle (vient de produire un SDK Alexa avec). Il est basé sur TLA + et tente d'apporter la puissance de ce travail incroyable à chaque développeur. SAM corrige trois approximations que (pratiquement) tout le monde utilise depuis des décennies: - les actions peuvent manipuler l'état de l'application - les affectations sont équivalentes à la mutation - il n'y a pas de définition précise de ce qu'est une étape de programmation (par exemple, est une étape = b * ca , sont 1 / lire b, c 2 / calculer b * c, 3 / assigner a avec le résultat trois étapes différentes?
métaprogrammeur
20

Après avoir essayé les différentes approches populaires (créateurs d'actions, thunks, sagas, épopées, effets, middleware personnalisé), je sentais encore qu'il y avait peut-être des améliorations à apporter, j'ai donc documenté mon parcours dans cet article de blog, Où mettre 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 des épopées, des sagas, des middleware personnalisés.

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 comme le rebond, la limitation, l'annulation et uniquement en utilisant la réponse de la dernière demande (takeLatest). redux-logic encapsule votre code en vous fournissant cette fonctionnalité.

Cela vous libère pour mettre en œuvre votre logique métier de base comme vous le souhaitez. Vous n'avez pas besoin d'utiliser des observables ou des générateurs, sauf si vous le souhaitez. Utilisez des fonctions et des rappels, des promesses, des fonctions asynchrones (async / attente), etc.

Le code pour 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 à ce que Sebastian Lorber a décrit où vous pouviez limiter l'affichage à N éléments et faire pivoter tout ce qui était en file d'attente. exemple de notification redux-logic

J'ai une variété d' exemples live jsfiddle redux-logic ainsi que des exemples complets . Je continue de travailler sur des documents et des exemples.

J'adorerais entendre vos commentaires.

Jeff Barczewski
la source
Je ne suis pas sûr que j'aime votre bibliothèque mais j'aime votre article! Bravo, mec! Vous avez fait suffisamment de travail pour gagner du temps aux autres.
Tyler Long
2
J'ai créé un exemple de projet pour redux-logic ici: github.com/tylerlong/hello-async/tree/master/redux-logic Je pense que c'est un logiciel bien conçu et je ne vois aucun inconvénient majeur par rapport aux autres alternatives.
Tyler Long
9

Je comprends que cette question est un peu ancienne mais je vais introduire une autre solution utilisant aka redux-observable . Épique.

Citant la documentation officielle:

Qu'est-ce qui est observable de nouveau?

Middleware RxJS 5 pour Redux. Composez et annulez des actions asynchrones pour créer des effets secondaires et plus encore.

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

C'est une fonction qui prend un flux d'actions et renvoie un flux d'actions. Actions en entrée, actions en sortie.

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 les délais d'expiration, les retards, les intervalles et les demandes).

Permettez-moi de poster le code, puis d'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 vous pouvez le 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 niveau supérieur qui reçoit un flux d'actions et renvoie un flux d'actions, afin que vous puissiez l'utiliser avec la fabrique de middleware createEpicMiddleware . Dans notre cas, nous n'en avons besoin que d'un, nous n'avons donc que notre rootEpic , nous n'avons donc pas à combiner quoi que ce soit, mais c'est un fait bien connu.

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 qui est à peu près déclaratif!

Point 3. Explication racine par ligne 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!

cnexans
la source
Pourriez-vous expliquer ce que font les méthodes API spécifiques ici, par exemple switchMap?
Dmitri Zaitsev
1
Nous utilisons redux-observable dans notre application React Native sur Windows. C'est une solution d'implémentation élégante à un problème complexe et hautement asynchrone et a un support fantastique via leurs problèmes de canal Gitter et GitHub. La couche supplémentaire de complexité n'en vaut la peine que si vous arrivez au problème exact qu'il est censé résoudre, bien sûr.
Matt Hargett
8

Pourquoi cela devrait-il être si difficile? C'est juste une logique d'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 avertir un composant que l'heure a changé"

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

Peut-être que c'est bien de le nettoyer chaque fois qu'une nouvelle notification est affichée.

Quoi qu'il en soit, il devrait y en avoir quelque setTimeoutpart, 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é "notification fade out" est vraiment une préoccupation d'interface utilisateur. Cela simplifie donc les tests de votre logique métier.

Il ne semble pas logique de tester comment il est mis en œuvre. Il est logique de vérifier quand la notification doit expirer. Ainsi moins de code à stub, des tests plus rapides, un code plus propre.

Vanuan
la source
1
Cela devrait être la meilleure réponse.
mmla
6

Si vous souhaitez gérer la temporisation sur des actions sélectives, vous pouvez essayer le middleware approche . J'ai rencontré un problème similaire pour gérer les actions basées sur les 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
})

le délai d'expiration 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. (géré dans le middleware)
  • non défini - pour envoi immédiat

L'implémentation de votre 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 désormais acheminer toutes vos actions via cette couche middleware à l'aide de redux.

createStore(reducer, applyMiddleware(timeoutMiddleware))

Vous pouvez trouver des exemples similaires ici

Yash
la source
5

La façon appropriée de le faire est d'utiliser Redux Thunk qui est un middleware populaire pour Redux, selon la documentation de Redux Thunk:

"Le middleware Redux Thunk vous permet d'écrire des créateurs d'actions qui renvoient une fonction au lieu d'une action. Le thunk peut être utilisé pour retarder l'envoi d'une action, ou pour n'envoyer que 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 de condition.

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);
  };
}
Alireza
la source
4

C'est simple. Utilisez le package trim-redux et écrivez comme ceci dans componentDidMountou ailleurs et tuez-le componentWillUnmount.

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

componentWillUnmount() {
  clearTimeout(this.tm);
}
Mohmmad Ebrahimi Aval
la source
3

Redux lui-même est une bibliothèque assez bavarde, et pour de telles choses, vous devrez utiliser quelque chose comme Redux-thunk , qui donnera une dispatchfonction, 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 dans l'action asynchrone, qui peut demander des informations en arrière-plan, ou vérifier plus tard si la notification a été fermée manuellement.

Bloomca
la source