25 septembre 2018
All Star #1 : Rematch
7 minutes de lecture
Dans une galaxie lointaine, entre le nodemodules et les repository git, se trouve une myriade de librairies, certaines y errent, d'autres y brillent ravivées par les commits de valeureux développeurs. La série d'articles _All Star est l'occasion pour nous de présenter une librairie que nous avons aimée, testée et approuvée !
Aujourd'hui direction Rematch ✨.
Carte d'identité
Rematch permet d’utiliser Redux au sein de son projet avec un minimum de code tout en en adoptant les bonnes pratiques.
yarn add @rematch/core@next
🐙 GitHub : https://github.com/rematch/rematch
📒 Doc : https://rematch.gitbooks.io/rematch
🤓 Démo : https://codesandbox.io/s/o7vpxyrzjq
Pourquoi l'utiliser ?
Toute personne ayant déjà développé une application avec la librairie Redux vous le dira : c’est génial mais cela engendre beaucoup de code répétitif. Chaque action Redux implémentée est synonyme de :
- Créer les constantes des actions ;
- Créer les actions dispatcher ;
- Gérer les side effects (avec éventuellement les erreurs / chargement d'une requête) ;
- Écrire son reducer ;
- Se demander si on devrait pas abstraire tout ça 🤓 ;
- Recommencer ⤴
Le développeur indolent s'est arrêté à l'étape 5, tourmenté par la question existentielle de l'abstraction. Il peut alors lire la documentation de Redux "reducing boilerplate" et créer ses fonctions ou bien utiliser Rematch ! Ce dernier enveloppe Redux et en propose une API plus pratique et moins verbeuse permettant d'implémenter une action Redux très rapidement.
Le développeur est alors plus épanoui et son code plus concis 🌈.
Focus sur Rematch
Cet article implique une certaine connaissance de Redux afin d'apprécier la librairie Rematch. Si ce n'est pas le cas, je vous conseille de passer par la case Redux afin de bien comprendre son fonctionnement.
Mise en place
Comme pour Redux, la première étape consiste à créer et configurer son store. Avec Rematch nous allons utiliser la méthode init
(nous n'utilisons plus la méthode createStore
de Redux).
import { init } from '@rematch/core'
import * as models from './models'
const store = init({
models,
})
export default store
La méthode init
prend des models
qui, nous le verrons par la suite, représentent l'état de notre application et les méthodes pour modifier cet état (c'est-à-dire les reducers
et les fonctions impures).
Éventuellement, vous pouvez aussi surcharger la configuration de Redux via la propriété redux
:
const store = init({
models,
// optionnel
redux: {
initialState,
reducers,
middlewares,
enhancers,
rootReducers,
combineReducers,
createStore,
devtoolOptions,
},
})
C'est un bon point pour Rematch, car son usage ne bloque pas l'utilisation traditionnelle de Redux, les deux peuvent ainsi coexister 🤝. Par exemple, vous pouvez intégrer la librairie Redux Form de cette manière :
import { reducer as formReducer } from 'redux-form'
const store = init({
models,
redux: {
reducers: {
form: formReducer,
},
},
})
Jusqu'ici rien de bien différent de Redux, c'est dans la définition des modèles que Rematch tire son épingle du jeu.
Vos modèles
La force de Rematch est de regrouper le state, les reducers, les types et les actions synchrones / asychrones dans un seul endroit : le modèle. Ce dernier représente une partie de votre store
Redux. Un modèle présente 3 parties distinctes :
- Le state
- Les reducers
- Les effects (action asynchrone, généralement les appels à votre API)
Créons un modèle pour gérer les utilisateurs de notre application, en voici son squelette :
// models/users.js
export const users = {
state: {
// Initial state
},
reducers: {
// Pure functions
},
effects: (dispatch) => ({
// Impure functions (async actions)
}),
}
Ainsi pour récupérer une liste d'utilisateurs et l'ajouter au state, nous intégrons notre logique :
export const users = {
state: {
items: [],
},
reducers: {
setUsers(state, users) {
return {
...state,
items: users,
}
},
},
effects: (dispatch) => ({
async loadUsers(payload, rootState) {
// Appel asynchrone de votre API
const users = await Api.getUsers()
// Appel de votre reducer
dispatch.users.setUsers(users)
},
}),
}
export default users
Le modèle permet de centraliser en un seul endroit les informations, ce qui est à l'usage très pratique. On se passe par la même occasion de redux-thunk : nous utilisons maintenant async
/ await
pour appeler nos APIs.
Une fois vos modèles configurés, vous pouvez dès lors les utiliser dans vos vues !
Utiliser vos modèles
Il suffit de passer vos modèles à la méthode init
de Rematch et vous voilà prêt à utiliser vos actions :
import users from './models/users'
import bookmarks from './models/bookmarks'
const store = init({
models: { users, bookmarks },
})
export const { dispatch } = store
L'objet store
expose la fonction dispatch
permettant d'appeler vos actions d'une manière globale :
dispatch.users.loadUsers()
où bien via les fonctions mapDispatchToProps
et mapStateToProps
habituelles de Redux (personnellement, je préfère cette méthode plus standard) :
import React from 'react'
import { connect } from 'react-redux'
const UserList = (props) => (
<div>
<button onClick={props.loadUsers}>Charger</button>
<ul>
{props.users.map((user) => (
<li>{user.name}</li>
))}
</ul>
</div>
)
const mapStateToProps = (state) => ({
users: state.users.items,
})
const mapDispatchToProps = (state) => ({
loadUsers: state.users.loadUsers,
})
const UserListContainer = connect(mapStateToProps, mapDispatchToProps)(UserList)
C'est tout ! Rematch expose automatiquement les actions (quelles soient pures ou impures). Vous vous épargnez l'écriture des types et des actions creators.
Voilà en quoi se résume Rematch ! Une manière élégante d'utiliser et alimenter l'état de votre application Redux.
🍒 Dernière cerise sur le gâteau : Rematch offre un sytème de plugins, dont un très pratique : le plugin Loading.
Plugins Rematch
Les plugins sont des packages non inclus par défaut dans la librairie Rematch, apportant de nouvelles fonctionnalités. Vous ne le savez pas encore mais la méthode init
expose également une propriété plugins
:
const store = init({
models,
plugins: [], // <-- Plugins door ✨
})
Plugin Loading
Comme vous aimez développer des interfaces fonctionnelles, vous affichez des spinners afin de signaler les périodes de chargement. Avec Redux, cela se fait généralement par l'ajout d'une propriété isLoading
dans vos stores que vous modifiez selon l'état de vos requêtes HTTP. Encore une fois, cela est un peu pénible et redondant à mettre en place (vivement React Suspense 🙌).
Le plugin Loading apporte une solution simple ! Pour l'ajouter, commencez par un habituel :
yarn add @rematch/loading
puis ajoutez-la à votre méthode init
:
import createLoadingPlugin from '@rematch/loading'
const options = {} // https://rematch.gitbooks.io/rematch/plugins/loading/#options
const loading = createLoadingPlugin(options)
const store = init({
models,
plugins: [loading], // <-- Plugins door ✨
})
Le plugin va alors automatiquement maintenir dans votre store un objet loading
reflétant l'état de chargement de vos modèles. En reprenant l'example des utilisateurs, voici à quoi ressemble votre store :
{
// Modèle users
users: {
items: []
},
// Géré par le plugin Loading
loading: {
global: false,
models: {
users: false
},
effects: {
users: {
loadUsers: false
}
}
}
}
Tous ces booléens sont automatiquement mis à jour, il y a trois niveaux de granularité pour chaque modèle :
global
est àtrue
si au moins un deseffects
parmis tous les modèles est en cours de chargement (c'est-à-dire leawait
pas résolu) ;models.users
est àtrue
si au moins un deseffects
du modèle users est en cours de chargement ;effects.users.loadUsers
est àtrue
si la méthodeloadUsers
est en cours de chargement.
Il vous suffit alors d'écouter le store loading
pour synchroniser vos UIs :
const UserList = (props) => (
<div>
<button disabled={props.isLoading} onClick={props.loadUsers}>
Charger
</button>
<ul>
{props.users.map((user) => (
<li>{user.name}</li>
))}
</ul>
</div>
)
const mapStateToProps = (state) => ({
users: state.users.items,
isLoading: state.loading.effects.users.loadUsers, // ✨ WOW ✨
})
const mapDispatchToProps = (state) => ({
loadUsers: state.users.loadUsers,
})
const UserListContainer = connect(mapStateToProps, mapDispatchToProps)(UserList)
Par défaut, le plugin gère les loaders pour tous vos modèles mais vous pouvez indiquer une whitelist / blacklist via les options du plugin :
{ whitelist: ['user/loadUsers'] })
{ blacklist: ['user/loadUsers'] })
La documentation du plugin est dispo ici !
Autres plugins
Sachez qu'il existe d'autres plugins "officiels" tels que :
- Rematch Persist permet d'intégrer en deux lignes redux-persist (pour synchroniser vos stores avec votre local storage 🙌) ;
- Rematch Select apporte les sélecteurs à vos modèles ;
- Rematch Updated permet d'horodater les changements dans vos modèles (à des fins d'optimisation).
Vous pouvez bien sûr écrire vos propres plugins 🤓.
En résumé
J'étais sceptique quant à l'utilisation d'un "framework Redux", redoutant d'introduire une grosse dépendance dans mes projets. Mais Rematch n'est pas du tout invasif et permet de continuer à utiliser Redux normalement si le besoin se présente et apporte un gros gain de productivité.
Enfin, voici un CodeSandbox reprenant le code de cet article. On se retouve bientôt pour un prochain All Star !
Longue vie et prospérité 🖖