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 showNotificationWithTimeout
sans 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 dispatch
comme premier argument? Parce qu'il doit 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é à partir d'un module, vous pouvez simplement l'importer 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 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 dispatch
tour. 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 dispatch
comme 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 dispatch
d'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 dispatch
en 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 dispatch
comme premier argument. Au lieu de cela, 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 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 dispatch
mé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 getState
comme 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 dispatch
mé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.
redux-saga
ré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