On parle beaucoup du dernier gosse de la ville de Redux en ce moment, redux-saga/redux-saga . Il utilise les fonctions du générateur pour écouter/répartir les actions.
Avant de commencer, j'aimerais connaître le pour et le contre de l'utilisation de redux-saga
au lieu de l'approche ci-dessous où j'utilise redux-thunk
avec async/wait.
Un composant peut ressembler à ceci, les actions de dispatch comme d'habitude.
import { login } from 'redux/auth';
class LoginForm extends Component {
onClick(e) {
e.preventDefault();
const { user, pass } = this.refs;
this.props.dispatch(login(user.value, pass.value));
}
render() {
return (<div>
<input type="text" ref="user" />
<input type="password" ref="pass" />
<button onClick={::this.onClick}>Sign In</button>
</div>);
}
}
export default connect((state) => ({}))(LoginForm);
Ensuite, mes actions ressemblent à ceci:
// auth.js
import request from 'axios';
import { loadUserData } from './user';
// define constants
// define initial state
// export default reducer
export const login = (user, pass) => async (dispatch) => {
try {
dispatch({ type: LOGIN_REQUEST });
let { data } = await request.post('/login', { user, pass });
await dispatch(loadUserData(data.uid));
dispatch({ type: LOGIN_SUCCESS, data });
} catch(error) {
dispatch({ type: LOGIN_ERROR, error });
}
}
// more actions...
// user.js
import request from 'axios';
// define constants
// define initial state
// export default reducer
export const loadUserData = (uid) => async (dispatch) => {
try {
dispatch({ type: USERDATA_REQUEST });
let { data } = await request.get(`/users/${uid}`);
dispatch({ type: USERDATA_SUCCESS, data });
} catch(error) {
dispatch({ type: USERDATA_ERROR, error });
}
}
// more actions...
Dans redux-saga, l'équivalent de l'exemple ci-dessus serait
export function* loginSaga() {
while(true) {
const { user, pass } = yield take(LOGIN_REQUEST)
try {
let { data } = yield call(request.post, '/login', { user, pass });
yield fork(loadUserData, data.uid);
yield put({ type: LOGIN_SUCCESS, data });
} catch(error) {
yield put({ type: LOGIN_ERROR, error });
}
}
}
export function* loadUserData(uid) {
try {
yield put({ type: USERDATA_REQUEST });
let { data } = yield call(request.get, `/users/${uid}`);
yield put({ type: USERDATA_SUCCESS, data });
} catch(error) {
yield put({ type: USERDATA_ERROR, error });
}
}
La première chose à noter est que nous appelons les fonctions api sous la forme yield call(func, ...args)
. call
n'exécute pas l'effet, il crée simplement un objet simple comme {type: 'CALL', func, args}
. L'exécution est déléguée au middleware redux-saga qui se charge de l'exécution de la fonction et de la reprise du générateur avec son résultat.
Le principal avantage est que vous pouvez tester le générateur en dehors de Redux en utilisant de simples contrôles d'égalité.
const iterator = loginSaga()
assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))
// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
iterator.next(mockAction).value,
call(request.post, '/login', mockAction)
)
// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
iterator.throw(mockError).value,
put({ type: LOGIN_ERROR, error: mockError })
)
Notez que nous nous moquons du résultat de l'appel api en injectant simplement les données simulées dans la méthode next
de l'itérateur. La simulation de données est beaucoup plus simple que les fonctions de simulation.
La deuxième chose à noter est l'appel à yield take(ACTION)
. Les créateurs d’action appellent des thunks à chaque nouvelle action (par exemple LOGIN_REQUEST
). c'est-à-dire que les actions sont continuellement poussées sur thunks, et que thunks n'a aucun contrôle sur le moment opportun pour arrêter de gérer ces actions.
Dans redux-saga, les générateurs tirent l'action suivante. c'est-à-dire qu'ils ont le contrôle quand il faut écouter pour une action et quand ne pas le faire. Dans l'exemple ci-dessus, les instructions de flux sont placées à l'intérieur d'une boucle while(true)
, de manière à ce que chaque action entrante soit écoutée, ce qui imite un peu le comportement d'appétit.
L'approche par traction permet de mettre en œuvre des flux de contrôle complexes. Supposons par exemple que nous voulions ajouter les exigences suivantes
Gestion de l'action utilisateur LOGOUT
lors de la première connexion réussie, le serveur renvoie un jeton qui expire dans un certain délai, stocké dans un champ expires_in
. Nous devrons actualiser l'autorisation en arrière-plan à chaque expires_in
millisecondes
Tenez compte du fait que lorsqu’il attend le résultat des appels de l’API (connexion initiale ou actualisation), l’utilisateur peut se déconnecter entre deux sessions.
Comment mettriez-vous cela en œuvre avec des thunks? tout en fournissant également une couverture de test complète pour l'ensemble du flux? Voici à quoi cela peut ressembler avec les Sagas:
function* authorize(credentials) {
const token = yield call(api.authorize, credentials)
yield put( login.success(token) )
return token
}
function* authAndRefreshTokenOnExpiry(name, password) {
let token = yield call(authorize, {name, password})
while(true) {
yield call(delay, token.expires_in)
token = yield call(authorize, {token})
}
}
function* watchAuth() {
while(true) {
try {
const {name, password} = yield take(LOGIN_REQUEST)
yield race([
take(LOGOUT),
call(authAndRefreshTokenOnExpiry, name, password)
])
// user logged out, next while iteration will wait for the
// next LOGIN_REQUEST action
} catch(error) {
yield put( login.error(error) )
}
}
}
Dans l'exemple ci-dessus, nous exprimons notre exigence de concurrence en utilisant race
. Si take(LOGOUT)
gagne la course (l’utilisateur a cliqué sur un bouton de déconnexion). La course annulera automatiquement la tâche en arrière-plan authAndRefreshTokenOnExpiry
. Et si la authAndRefreshTokenOnExpiry
était bloquée au milieu d'un appel call(authorize, {token})
, elle serait également annulée. L'annulation se propage automatiquement vers le bas.
Vous pouvez trouver un démo exécutable du flux ci-dessus
J'ajouterai mon expérience d'utilisation de saga dans le système de production à la réponse plutôt détaillée de l'auteur de la bibliothèque.
Pro (en utilisant la saga):
Testabilité. Il est très facile de tester les sagas car call () renvoie un objet pur. Pour tester thunks, vous devez normalement inclure un mockStore dans votre test.
redux-saga contient de nombreuses fonctions utiles sur les tâches. Il me semble que le concept de saga est de créer un type de travailleur/fil d’arrière-plan pour votre application, qui fait office de pièce manquante dans l’architecture redux de réaction (actionCreators et réducteurs doivent être des fonctions pures.) Ce qui conduit au point suivant.
Les sagas offrent un endroit indépendant pour gérer tous les effets secondaires. Il est généralement plus facile de modifier et de gérer que les actions simples dans mon expérience.
Con:
Syntaxe du générateur.
Beaucoup de concepts à apprendre.
Stabilité de l'API. Il semble que redux-saga ajoute encore des fonctionnalités (par exemple, des chaînes?) Et la communauté n’est pas aussi importante. Il y a un problème si la bibliothèque effectue un jour une mise à jour non compatible avec les versions antérieures.
J'aimerais juste ajouter quelques commentaires de mon expérience personnelle (en utilisant à la fois des sagas et du thunk):
Les Sagas sont super à tester:
Les Sagas sont plus puissants. Tout ce que vous pouvez faire dans le créateur d'action d'un thunk peut également l'être dans une saga, mais pas l'inverse (ou du moins, pas facilement). Par exemple:
take
)cancel
, takeLatest
, race
)take
, takeEvery
, ...)Sagas propose également d’autres fonctionnalités utiles, généralisant certains modèles d’application courants:
channels
pour écouter des sources d'événements externes (par exemple, des websockets)fork
, spawn
)Les Sagas sont un outil formidable et puissant. Cependant, avec le pouvoir vient la responsabilité. Lorsque votre demande augmente, vous pouvez facilement vous perdre en déterminant qui attend l'envoi de l'action ou tout ce qui se passe lors de l'envoi d'une action. D'autre part, thunk est plus simple et plus facile à raisonner. Le choix de l'un ou de l'autre dépend de nombreux aspects, tels que le type et la taille du projet, les types d'effets secondaires que votre projet doit gérer ou les préférences de l'équipe de développement. Dans tous les cas, il suffit de garder votre application simple et prévisible.
Juste une expérience personnelle:
Pour ce qui est du style de codage et de la lisibilité, l’un des avantages les plus importants de l’utilisation de redux-saga dans le passé est d’éviter tout rappel par call-back dans redux-thunk - il n’est plus nécessaire d’utiliser beaucoup d’emboîtements à ce moment-là. Mais maintenant, avec la popularité de async/wait thunk, on peut aussi écrire du code asynchrone dans un style synchronisé avec redux-thunk, ce qui peut être considéré comme une amélioration de redux-think.
Vous devrez peut-être écrire beaucoup plus de code standard lorsque vous utiliserez redux-saga, en particulier dans TypeScript. Par exemple, si l'on souhaite implémenter une fonction d'extraction asynchrone, la gestion des données et des erreurs peut être effectuée directement dans une unité thunk dans action.js avec une seule action FETCH. Mais dans redux-saga, il peut être nécessaire de définir les actions FETCH_START, FETCH_SUCCESS et FETCH_FAILURE et toutes les vérifications de type correspondantes, car l’une des fonctionnalités de redux-saga est d’utiliser ce type de mécanisme riche de "jetons" pour créer des effets et Redux Store pour des tests faciles. Bien sûr, on pourrait écrire une saga sans utiliser ces actions, mais cela ressemblerait à un thunk.
En termes de structure de fichier, redux-saga semble être plus explicite dans de nombreux cas. On pourrait facilement trouver un code lié asynchrone dans chaque fichier sagas.ts, mais dans redux-thunk, il faudrait le voir dans les actions.
Des tests faciles peuvent constituer une autre caractéristique pondérée de Redux-Saga. C'est vraiment pratique. Une chose à clarifier toutefois est que le test “d'appel” de redux-saga n'effectue pas un appel API réel lors du test. Par conséquent, il est nécessaire de spécifier le résultat de l'échantillon pour les étapes pouvant l'utiliser après l'appel API. Par conséquent, avant d'écrire dans redux-saga, il serait préférable de planifier une saga et ses sagas.spec.ts correspondantes en détail.
Redux-saga fournit également de nombreuses fonctionnalités avancées telles que l'exécution de tâches en parallèle, des aides à la concurrence telles que takeLatest/takeEvery, fork/spawn, qui sont bien plus puissants que Thunks.
En conclusion, personnellement, je voudrais dire ceci: dans de nombreux cas normaux et dans des applications de taille petite à moyenne, optez pour le style async/wait style redux-thunk. Cela vous épargnerait de nombreux codes/actions/typedefs standard, et vous n'auriez pas besoin de changer de sagas.ts et de maintenir un arbre de sagas spécifique. Mais si vous développez une application volumineuse avec une logique asynchrone beaucoup complexe et que vous avez besoin de fonctionnalités telles que la configuration simultanée/parallèle, ou si vous avez une forte demande en tests et en maintenance (en particulier dans le développement piloté par les tests), redux-sagas vous sauverait probablement la vie. .
Quoi qu'il en soit, redux-saga n'est pas plus difficile et complexe que redux lui-même, et ne présente pas de courbe d'apprentissage dite abrupte, car ses concepts de base et API sont bien limités. Consacrer un peu de temps à l’apprentissage de redux-saga peut vous aider un jour dans le futur.
Un moyen plus simple consiste à utiliser redux-auto .
de la documantasion
redux-auto a résolu ce problème asynchrone simplement en vous permettant de créer une fonction "action" renvoyant une promesse. Pour accompagner la logique d'action de votre fonction "par défaut".
L'idée est d'avoir chaque action dans un fichier spécifique . co-localiser l'appel du serveur dans le fichier avec les fonctions de réduction pour "en attente", "rempli" et "rejeté". Cela rend les promesses de manipulation très faciles.
Il attache également automatiquement un objet d'assistance (appelé "asynchrone") au prototype de votre état, ce qui vous permet de suivre dans votre interface utilisateur les transitions demandées.
Thunks versus Sagas
Redux-Thunk
et Redux-Saga
diffèrent de manière importante, les deux étant des bibliothèques de middleware pour Redux (le middleware Redux est un code qui intercepte les actions entrant dans le magasin via la méthode dispatch ()).
Une action peut être littéralement n'importe quoi, mais si vous suivez les meilleures pratiques, elle est un objet JavaScript simple avec un champ de type et des champs facultatifs de charge utile, de méta et d'erreur. par exemple.
const loginRequest = {
type: 'LOGIN_REQUEST',
payload: {
name: 'admin',
password: '123',
}, };
Redux-Thunk
En plus de l'envoi d'actions standard, le middleware Redux-Thunk
vous permet d'envoyer des fonctions spéciales, appelées thunks
.
Thunks (en Redux) ont généralement la structure suivante:
export const thunkName =
parameters =>
(dispatch, getState) => {
// Your application logic goes here
};
C'est-à-dire que thunk
est une fonction qui prend (éventuellement) certains paramètres et retourne une autre fonction. La fonction interne prend les fonctions dispatch function
et getState
, qui seront toutes deux fournies par le middleware Redux-Thunk
.
Redux-Saga
Redux-Saga
le middleware vous permet d'exprimer une logique d'application complexe sous forme de fonctions pures, appelées sagas. Les fonctions pures sont souhaitables du point de vue des tests car elles sont prévisibles et répétables, ce qui les rend relativement faciles à tester.
Les Sagas sont implémentés via des fonctions spéciales appelées fonctions génératrices. Ce sont une nouvelle fonctionnalité de ES6 JavaScript
. En gros, l’exécution saute d’un générateur à l’autre où vous voyez une déclaration de rendement. Pensez à une instruction yield
comme provoquant une pause du générateur et le renvoi de la valeur renvoyée. Plus tard, l'appelant peut reprendre le générateur à l'instruction suivant le yield
.
Une fonction génératrice est définie comme celle-ci. Notez l'astérisque après le mot clé function.
function* mySaga() {
// ...
}
Une fois que la saga de connexion est enregistrée avec Redux-Saga
. Mais ensuite, la yield
prise sur la première ligne mettra la saga en pause jusqu'à ce qu'une action de type 'LOGIN_REQUEST'
soit envoyée au magasin. Une fois que cela se produit, l'exécution continuera.
Un petit mot. Les générateurs sont annulables, asynchrones/wait - pas. Ainsi, pour un exemple tiré de la question, cela n’a pas vraiment de sens. Mais pour des flux plus compliqués, il n’ya parfois pas de meilleure solution que d’utiliser des générateurs.
Donc, une autre idée pourrait être d’utiliser des générateurs avec redux-thunk, mais pour moi, c’est comme essayer d’inventer un vélo à roues carrées.
Et bien sûr, les générateurs sont plus faciles à tester.
Voici un projet qui combine les meilleures parties (pros) de redux-saga
et redux-thunk
: vous pouvez gérer tous les effets secondaires des sagas tout en obtenant une promesse de dispatching
l'action correspondante: - https://github.com/diegohaz/redux-saga-thunk
class MyComponent extends React.Component {
componentWillMount() {
// `doSomething` dispatches an action which is handled by some saga
this.props.doSomething().then((detail) => {
console.log('Yaay!', detail)
}).catch((error) => {
console.log('Oops!', error)
})
}
}
Après avoir passé en revue quelques projets React/Redux de grande envergure selon mon expérience, Sagas offre aux développeurs une manière plus structurée d’écrire du code, beaucoup plus facile à tester et plus difficile à obtenir.
Oui, c'est un peu bizarre au début, mais la plupart des développeurs en comprennent assez en une journée. Je dis toujours aux gens de ne pas s'inquiéter de ce que yield
fait au début et qu'une fois que vous aurez écrit un test, il vous sera envoyé.
J'ai vu quelques projets où des thunks ont été traités comme s'ils étaient des contrôleurs de MVC Patten, ce qui est rapidement devenu un gâchis incompréhensible.
Mon conseil est d'utiliser des sagas où vous avez besoin de déclencheurs de type B relatifs à un seul événement. Pour tout ce qui pourrait concerner plusieurs actions, j’ai trouvé qu’il était plus simple d’écrire un middleware client et d’utiliser la propriété méta d’une action de la FSA pour la déclencher.