Ne tombez pas dans le le piège de penser qu'une bibliothèque doit 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 fait offrent d'autres moyens de traiter les éléments asynchrones, mais vous ne devriez les utiliser que lorsque vous vous rendez compte que vous répétez trop de code. À moins que vous ne soyez confronté à ce problème, utilisez ce que le langage propose et optez pour la solution la plus simple.
Écrire du code asynchrone en ligne
C'est de loin la méthode la 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 vous obtenez soit dispatch()
ou des créateurs d'actions spécifiques injectés en tant que props. Cependant, cela ne fait aucune différence pour nous.
Si vous n'aimez pas faire des fautes de frappe en répartissant les mêmes actions à partir de différents composants, vous pouvez extraire les créateurs d'action 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 intergiciel ou autre concept avancé.
Extraction du créateur d'actions asynchrones
L'approche ci-dessus fonctionne bien dans les cas simples, mais vous pouvez constater qu'elle présente quelques problèmes :
- Cela vous oblige à dupliquer cette logique partout où vous voulez afficher une notification.
- Les notifications n'ont pas d'ID, ce qui entraîne une situation de course si vous affichez deux notifications assez rapidement. Lorsque le premier délai d'attente se termine, il distribue
HIDE_NOTIFICATION
en cachant par erreur la deuxième notification avant la fin du délai.
Pour résoudre ces problèmes, il faudrait extraire une fonction qui centralise la logique du délai d'attente et dispatche 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)
}
Les composants peuvent maintenant utiliser showNotificationWithTimeout
sans dupliquer cette logique ou avoir des conditions de course 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 est-ce que showNotificationWithTimeout()
accepter dispatch
comme 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 la répartition.
Si vous aviez un magasin singleton exporté par un module, vous pourriez simplement l'importer et dispatch
directement sur elle à 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 ne l'aimons pas est que cela oblige le magasin à être un singleton . Cela rend la mise en œuvre très difficile rendu du serveur . Sur le serveur, vous voudrez que chaque requête ait son propre magasin, de sorte 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 simuler un magasin lorsque vous testez des créateurs d'action parce qu'ils font référence à un magasin réel spécifique exporté par 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 magasin singleton à partir d'un module, nous vous déconseillons de le faire. Ne le faites 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.
Logiciel intermédiaire Thunk
Pour les applications simples, cette approche devrait suffire. Ne vous préoccupez pas de l'intergiciel si vous en êtes satisfait.
Dans les applications plus importantes, cependant, vous pourriez trouver certains inconvénients.
Par exemple, il semble malheureux que nous devions passer dispatch
autour. Il est donc plus difficile de séparer le conteneur et les 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 pour qu'il puisse le passer plus loin. Vous ne pouvez pas simplement lier les créateurs d'action avec connect()
plus parce que showNotificationWithTimeout()
n'est pas vraiment un créateur d'actions. Il ne retourne pas une action Redux.
En outre, il peut être difficile de se rappeler quelles fonctions sont des créateurs d'actions synchrones comme showNotification()
et qui sont des aides asynchrones comme showNotificationWithTimeout()
. Vous devez les utiliser différemment et faire attention à ne pas les confondre.
C'est ce qui a motivé trouver un moyen de "légitimer" ce mode de fourniture dispatch
à une fonction d'aide, et aider Redux à "voir" de tels créateurs d'actions asynchrones comme un cas spécial de créateurs d'actions normaux. plutôt que des fonctions totalement différentes.
Si vous êtes toujours parmi nous et que vous reconnaissez également un problème dans votre application, vous pouvez utiliser le bouton Redux Thunk intergiciel.
En résumé, Redux Thunk apprend à 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 lui donnera dispatch
comme un argument. Il "avale" également de telles actions ; ne vous inquiétez donc pas de voir vos réducteurs recevoir des arguments de fonction bizarres. 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 showNotificationWithTimeout()
comme un 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 que nous avons écrite dans la section précédente. Cependant, elle n'accepte pas dispatch
comme premier argument. Au lieu de cela, il renvoie à une fonction qui accepte dispatch
comme premier argument.
Comment l'utiliserions-nous dans notre composante ? Sans aucun doute, 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
et ensuite on passe dispatch
.
Cependant, c'est encore plus gênant que la version originale ! Pourquoi avons-nous fait ça ?
A cause de ce que je vous ai dit avant. Si le middleware Redux Thunk est activé, chaque fois que vous essayez de distribuer une fonction au lieu d'un objet d'action, le middleware appellera cette fonction avec dispatch
elle-même comme premier argument .
On peut donc faire ça à la place :
// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
Enfin, l'envoi d'une action asynchrone (en fait, une série d'actions) ne semble pas différent de l'envoi d'une action unique de manière synchrone au composant. C'est une bonne chose car les composants ne devraient pas se soucier de savoir si quelque chose se passe de manière synchrone ou asynchrone. Nous avons simplement fait abstraction de cela.
Remarquez que depuis que nous avons "appris" à Redux à reconnaître de tels créateurs d'actions "spéciales" (nous les appelons jeté ), nous pouvons maintenant les utiliser dans tous les endroits où nous utiliserions des créateurs d'action 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)
Lire l'État dans les Thunks
En général, vos reducers contiennent la logique métier permettant de déterminer l'état suivant. Cependant, les réducteurs n'interviennent qu'après la distribution des actions. Que faire si vous avez un effet secondaire (tel que l'appel d'une API) dans un créateur d'action thunk, et que vous voulez l'empêcher sous certaines conditions ?
Sans utiliser le middleware thunk, il suffit de faire cette vérification à l'intérieur du composant :
// component.js
if (this.props.areNotificationsEnabled) {
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}
Cependant, l'intérêt 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 du magasin Redux. En plus de dispatch
il passe également getState
comme deuxième argument de la fonction que vous retournez depuis 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. Il permet d'éviter les appels d'API lorsque des données sont disponibles en mémoire cache, mais il ne constitue pas une très bonne base pour construire votre logique métier. Si vous utilisez getState()
seulement pour distribuer conditionnellement différentes actions, envisagez plutôt de placer la logique commerciale dans les réducteurs.
Les prochaines étapes
Maintenant que vous avez une intuition de base sur le fonctionnement des thunks, découvrez 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 cela 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 à partir de dispatch()
. C'est pourquoi vous pouvez renvoyer une Promise depuis un thunk et attendre qu'il se termine en appelant dispatch(someThunkReturningPromise()).then(...)
.
Vous pouvez également diviser les créateurs d'actions thunk complexes en plusieurs créateurs d'actions thunk plus petits. Le site dispatch
fournie par thunks peut accepter thunks lui-même, de sorte que vous pouvez appliquer le motif de manière récursive. Encore une fois, cela fonctionne mieux avec les Promesses car vous pouvez implémenter un flux de contrôle asynchrone par-dessus.
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 par des thunks. Par exemple, la relance des requêtes qui ont échoué, le flux de réautorisation avec des jetons ou l'accueil étape par étape peuvent être trop verbeux et sujets à des erreurs lorsqu'ils sont écrits de cette façon. Dans ce cas, vous pouvez vous tourner vers des solutions de flux de contrôle asynchrones plus avancées, telles que les suivantes Saga Redux ou Boucle Redux . Évaluez-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 réellement besoin. N'oubliez pas que, selon les besoins, votre solution peut être aussi simple que
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Ne vous en faites pas si vous ne savez pas pourquoi vous faites ça.
35 votes
N'oubliez pas de consulter mon
redux-saga
si vous voulez quelque chose de mieux que les thunks. Une réponse tardive qui vous oblige à faire défiler les pages longtemps avant de la voir apparaître :) ne signifie pas qu'elle ne vaut pas la peine d'être lue. Voici un raccourci : stackoverflow.com/a/38574266/826098 votes
Chaque fois que vous faites setTimeout, n'oubliez pas d'effacer le temporisateur en utilisant clearTimeout dans la méthode du cycle de vie componentWillUnMount.
3 votes
Redux-saga est cool mais ils ne semblent pas avoir de support pour les réponses typées des fonctions de générateur. Cela pourrait avoir de l'importance si vous utilisez typescript avec react.