All Star #1 : Rematch

8 minutes de lecture

All Star #1 : Rematch

Dans une galaxie lointaine, entre le node_modules 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 :

  1. Créer les constantes des actions ;
  2. Créer les actions dispatcher ;
  3. Gérer les side effects (avec éventuellement les erreurs / chargement d’une requête) ;
  4. Écrire son reducer ;
  5. Se demander si on devrait pas abstraire tout ça 🤓 ;
  6. 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 :

  1. global est à true si au moins un des effects parmis tous les modèles est en cours de chargement (c’est-à-dire le await pas résolu) ;
  2. models.users est à true si au moins un des effects du modèle users est en cours de chargement ;
  3. effects.users.loadUsers est à true si la méthode loadUsers 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é 🖖

Continuer la discussion sur Twitter