jl
Mer. 18 Mai 2016

React + Redux, une mise en bouche

Après avoir parlé de React dans un article précédent, il est temps de passer à Redux de manière à créer des applications web et des interfaces toujours plus interactives.

Aujourd'hui, nous allons partir d'un cas pratique de manière à appréhender Redux ? Nous allons donc réaliser un petit chronomètre de manière à mettre en avant les avantages fournis par cette librairie et cette vision particulière.

Redux en 2 minutes

Les termes utilisés sont volontairement en anglais.

Avant toute chose, il va nous falloir introduire le jargon propre à l'architecture proposée par facebook : Flux. Pourquoi nous parlons de Flux ? Tout simplement parce que Redux est une implémentation de ce type d'architecture.

Le but de cette architecture est d'avoir un état “client” de l'application qui va diriger son rendu. Ainsi, lorsque l'état change, les composants sont notifiés et se mettent à jour si nécessaire. Et nous allons le voir, cet état ne peut pas être modifié n'importe comment.

Store

Au centre de Redux, il y'a le store. Il s'agit de l'état de notre application et il ne peut en exister qu'un seul. Il ne doit être modifié que par les réducteurs. C'est sur cet état que nos composants vont venir se connecter pour se ré-afficher si besoin est.

Actions

Les actions permettent de modifier le store de l'application. A chaque fois que vous devez modifier l'état de l'application, vous devez donc passer par une action. Il est interdit de modifier directement l'état. Ce sont de simples objets javascript avec une propriété type.

Mais ce ne sont pas les actions qui décrivent comment modifier l'état, elles sont là uniquement pour indiquer un changement d'état. Ce sont les réducteurs qui se chargent de traduire ces actions en modifications dans l'état.

Reducers

Les reducers (réducteurs) décrivent donc comment l'état est modifié en réponse à des actions. Ces réducteurs prennent la forme de fonction pure acceptant en paramètres l'état courant et l'action venant d'être dispatchée et renvoyant en retour la modification de l'état qui sera fusionnée dans l'état global.

Si tout ceci vous semble encore abstrait, il est temps de passer à la pratique !

L'environnement et notre état

Étant donné que nous nous intéressons aujourd'hui à Redux, la mise en place de l'environnement ne sera pas détaillé ici (il s'agit grossièrement de la même chose que l'article précédent) mais vous pouvez retrouver les sources de ce projet sur mon Github.

Avant de commencer à rentrer dans le code, il faut se poser la question du design de notre état, c'est à dire de quoi avons nous besoin pour que cette application fonctionne.

De mon côté, j'ai identifié 2 choses, les timers qui représentent les chronos déjà effectués que je peux supprimer et un timer qui représente l'état du timer courant. Aussi, je viens d'exprimer mes réducteurs !

L'état n'est qu'un simple objet javascript qui pourrait ressembler à ceci :

var store = {
  timer: {
    startedAt: null, // Date de début du timer
    isRunning: false, // Est-ce qu'il est en train d'être incrémenté
    value: 0 // La valeur (en ms) actuelle
  },
  timers: {
    1: { // La clé représente l'id du chrono, c'est plus pratique pour les actions qui ciblent en général un id, cela nous permet d'éviter des parcours inutiles
      // Propriétés identiques au timer
    },
    2: {
      // ...
    },
    // ...
  }
}

Nous n'allons cependant pas créer le store de cette manière, nous allons utiliser la librairie redux ainsi que nos réducteurs pour arriver à cela. De cette manière, le store pourra dispatcher des actions et appeler les réducteurs qui vont bien.

Le point d'entrée

Au commencement, il y'a notre fichier src/main.js, jetons-y un coup d'œil :

import React from 'react';
import { render } from 'react-dom';
import { createStore, combineReducers, applyMiddleware} from 'redux';
import { Provider } from 'react-redux';
import { timersReducer, timerReducer } from './timers_reducer';
import thunk from 'redux-thunk';

const store = createStore(combineReducers({
    timers: timersReducer,
    timer: timerReducer
}), applyMiddleware(thunk));

import TimerPanel from './TimerPanel';
import TimersList from './TimersList';
import './main.scss';

render(
    <Provider store={store}>
        <div className="stopwatch">
            <TimerPanel />
            <TimersList />
        </div>
    </Provider>
, document.getElementById('root'));

On importe toutes les librairies dont nous allons avoir besoin, on configure notre store puis on affiche nos composants.

La création du store

Nous créons donc notre store grâce à la fonction createStore de redux. En passant nos réducteurs à combineReducers, redux peut construire un seul objet dont les propriétés sont issues de différents réducteurs. La fonction createStore accepte aussi d'autres paramètres de manière à donner de nouvelles compétences au store.

On applique ici le thunk middleware de manière à pouvoir gérer des dispatch asynchrones grâce aux actions creators.

À la création du store, Redux va appelé les différents réducteurs de manière à obtenir l'état initial de notre application, ce qui va aboutir à quelque chose dans le genre de ceci :

const store = {
  timer: {
    startedAt: null, // Date de début du timer
    isRunning: false, // Est-ce qu'il est en train d'être incrémenté
    value: 0 // La valeur (en ms) actuelle
  },
  timers: { }
}

Attention toutefois, cet état est englobé dans un objet spécial qui permet d'appelé par exemple la méthode store.dispatch pour faire traverser une action dans les différents réducteurs et ainsi modifier l'état de l'application. Il est donné ici à titre pûrement indicatif.

L'affichage des composants

Pour finir, on importe et affiche nos composants qui sont des composants sans états. J'y reviendrai pas la suite.

Les réducteurs

Lors de l'appel au createStore, redux demande l'état initial de manière à initialiser l'état de l'application. Voyons voir à quoi ressemblent nos réducteurs. Prenons le fichier timers_reducer.js :

import * as actions from './timers_actions';

const timerInitialState = {
    startedAt: null,
    isRunning: false,
    value: 0
}

export const timerReducer = (state = timerInitialState, action) => {
    switch (action.type) {
        case actions.START_TIMER:
            return { ...timerInitialState, startedAt: action.startedAt, isRunning: true };
        case actions.STOP_TIMER:
            return { ...state, isRunning: false };
        case actions.UPDATE_TIMER_VALUE:
            return { ...state, value: action.value };
        default:
            return state;
    }
}

export const timersReducer = (state = {}, action) => {
    const newState = { ...state };

    switch (action.type) {
        case actions.ADD_TIMER:
            newState[action.timer.id] = action.timer;
            return newState;
        case actions.REMOVE_TIMER:
            delete newState[action.id];
            return newState;
        default:
            return state;
    }
}