Architecture Redux

June 17, 2016

React fournit seulement un moyen de dessiner de manière efficace des composants en fonction de données d’entrées.

Flux est un pattern permettant de gérer l’état d’une application qui garanti un flux de données unidirectionnel (one way databinding) Redux est l’implémentation la plus populaire.

Redux, met en scène 3 principes :

  • une seule source de vérité : le state de l’application est maintenu dans une structure de données à l’intérieur d’un seul store
  • le state est immutable : La seule manière de modifier le state est via l’émission d’une action, un objet décrivant la modification à apporter. Toutes les modifications sont centralisées et se produisent une à une, évitant ainsi les problèmes de concurrence
  • les modifications sont effectuées à l’aide de fonction pures appelées reducers
Architecture Redux

Le schéma illustre le flux unidirectionnel des données dans cette architecture : des actions sont dispatchées et traitées par le reducer, qui se charge de mettre à jour le store. Toutes les vues (ici les composants react) abonnées au store se mettent à jour en conséquence. Ces vues peuvent également dispatcher des actions et ainsi de suite.

Actions et Actions creators

Les actions sont des paquets de données envoyés au store. Elles sont la seule source d’information du store. Une action est envoyée au store grâce à la fonction store.dispatch.

Voici un exemple d’action qui représente le changement de nom d’une personne :

{
  type: 'CHANGE_NAME',
  payload: 'Vince'
}

Si cette action se révèle être utilisée souvent, nous pouvons écrire une fonction qui se chargera de la créer.

function changeName(name) {
  return {
    type: 'CHANGE_NAME',
    payload: name,
  }
}

On appelle ces fonctions des action creator. Elles rendent les actions réutilisables et facilement testables. Les actions peuvent être “dispatchées” avec : dispatch(changeName('Vincent'))

Reducer

Les actions décrivent le fait que quelque chose s’est passé mais ne spécifient pas la manière dont le store doit être modifié. C’est le rôle du reducer, une fonction pure qui prend en paramètre le state, une action, et retourne le nouveau state.

(previousState, action) => nextState

Le reducer est une fonction pure, par conséquent il ne doit jamais:

  • modifier directement ses arguments
  • effectuer des opérations ayant des effets de bord tel que des appels à une api
  • appeler des fonctions impures telles que Date.now() etc…

Il est uniquement chargé de calculer le nextState.

✘ Exemple: un reducer incorrect, mutation du state INTERDITE!

Le state est muté. La propriété du state étant modifiée directement (l.4), les composants abonnés à cette partie du state ne se mettrons pas à jour et ignorerons cette modification.

function user(state = {}, action) {
  switch (action.type) {
    case 'CHANGE_NAME':
      state.name = action.name // INTERDIT !!
      return state 
    default:
      return state
  }
}

✔ Exemple Un reducer correct

function user(state = {}, action) {
  switch (action.type) {
    case 'CHANGE_NAME':
      return {
        ...state,
        name: action.name,
      }
    default:
      return state
  }
}

Note : On utilise ici l’opérateur object spread ..., une syntaxe d’ECMAScript 2016, qui permet de copier les propriétés d’un objet dans un nouvel objet d’une manière plus succincte. Nous pouvons également utiliser des bibliothèques qui garantissent l’immutabilité telles que immutable.js développée par Facebook

Store

Le store est un objet qui :

  • maintient le state de l’application
  • permet l’accès à ce state via getState()
  • permet de mettre à jour le state via dispatch(action)
  • permet d’abonner des composants via subscribe(listener) (composants notifiés lorsque le state subit une modification)

Async Actions

Afin d’orchestrer des flux asynchrones (par exemple, les appels réseaux) nous pouvons utiliser le middleware Redux-thunk. Ce _middleware permet de traiter les actions étant des fonctions (appelées thunk action). Une action thunk ne doit pas forcément être pure et peut avoir des effets de bords. Les fonctions dispatch et getState du store lui sont passées en argument, ce qui lui donne la possibilité de dispatcher d’autres actions et d’accéder au state.

Exemple d’un thunk action creator qui renvoie une fonction :

function whatIsMyName() {
  return async (dispatch, getState) => {
    dispatch(fetchNameRequest())
    try {
      const res = await fetch('http://vincent.cordobes/name')
      const name = await res.json()
      dispatch(fetchNameSuccess(name))
    } catch (err) {
      dispatch(fetchNameError(err))
    }
  }
}

L’exemple ci-dessus met en évidence une action creator qui retourne une fonction. Des actions marquant le début, le succès ou une erreur de l’appel (l.5) à l’API sont “dispatchées” (l.3, l.7, l.9) permettant de mettre à jour le store en fonction de l’avancement de la requête.

Remarques relativement au code ci-dessus : syntaxe avec les mots clés async/await. Cette syntaxe fait son apparition dans ECMAScript 2017. En résumé, await permet d’attendre la résolution d’une promesse et ne peux être utilisé que dans une fonction préfixée par async (elle-même renverra à son tour une promesse) Il permet d’écrire le code asynchrone de javascript à la manière d’un code synchrone.

Selecteurs

Afin de comprendre l’utilité des sélecteurs, prenons un exemple. Considérons une liste de personnes, une recherche (par nom) et des filtres (sexe, age, etc…) sur ces personnes.

En suivant les principes Redux, le store contient les données et les critères de recherche. À partir de ces éléments nous pouvons calculer la liste filtrée à afficher.

Une bonne pratique, concernant le state, est de contenir seulement des donnée minimisée, c’est-à-dire des données ne pouvant pas être obtenues à partir d’autres données. Les états dérivés ne doivent pas être présents dans le state.

React filtre la data au render

Le bon endroit pour filtrer et afficher cette liste est donc la méthode render. Ainsi, si un critère de recherche ou si les données changent, le composant exécute la méthode render, filtre les données et les affiche. Il en résulte une UI toujours synchronisée avec le state.

Cette technique présente néanmoins un inconvénient. Supposons qu’une props autre que les filtres et la liste de personnes, change : le filtrage de la liste se fera donc, inutilement, à chaque update du composant.

La complexité de ce filtrage étant du O(n)O(n), cela n’est pas très gênant si la taille des données à filtre reste modérée.

Cependant, des listes de données potentiellement grandes ou même un calcul plus complexe dégraderaient fortement les performances de l’application.

C’est ici qu’entrent en jeu les selectors :

Un selecteur filtre la data pour la passer au composant

Les selectors calculent des données dérivées. Ils permettent au state de ne stocker que les données minimisée. Ils sont efficaces et ne sont pas recalculés si les arguments restent les mêmes → ils sont mémoisés. Enfin ils sont composables, c’est-à-dire qu’ils peuvent être utilisés en entrées d’autres selectors. Ainsi toute la complexité est déplacée à l’extérieur et prise en charge par les selectors,

Les selectors jouent le rôle d’api, permettant un accès au state. Les composants React ne connaissent que cette interface. Une conséquence directe est le découplage de ces composants vis-à-vis de la forme du state. Un autre bénéfice est la simplification du code des composants React.

Exemple avec la bibliothèque reselect

Définition des selectors
const getUsers = state => state.users
const getSearchTerm = state => state.searchTerm

// Memoized selector
const getFilteredUsers = createSelector(
  getUsers,
  getSearchTerm,
  (users, searchTerm) => users.filter(
    user => user.indexOf(searchTerm) > -1
  )
);
Définition du composant React
const UserList = ({ filteredUsers }) => (
  <ul>
    {filteredUsers.map(user => <li>{user}</li>)}
  </ul>
);
Connexion du composant
export default connect(state => (
  filteredUsers: getFilteredUsers(state)
))(UserList);

Note : Redux est une bibliothèque dogmatique mettant en scène plusieurs concepts et patterns (immutabilités, flux unidirectionnel etc…) et ces principes sous-jacents peuvent parfaitement s’appliquer à d’autres architectures.

Références

  • Redux doc. https://redux.js.org
  • React and flux in production best practices. https://medium.com/@delveeng/react-and-flux-in-production-best-practices-c87766c57cb6#.elbdrmo4f

Written by Vincent Cordobes