Introcution à Redux

Redux (http://redux.js.org/) est une bibliothèque JS permettant de gérer l’état d’une application de manière déterministe.

Redux propose un conteneur (le store) dont les modifications sont décrites par des actions (sortes d’évènements) qui sont gérés par un réducteur (reducer).

Les Actions

Une action est un simple objet JS qui a pour seule contrainte d’avoir une propriété type sérialisable (un string) ainsi que n’importe quelles autres propriétés permettant au réducteur de générer un nouvel état.

const action = {
  type: 'CHANGE_MENU_SELECTION',
  selectedMenuItem: '#about_menu'
}

On note que le type de l’action contient un verbe actif. Il doit permettre d’identifier la nature de la modification correspondant à l’action.

Le Réducteur (reducer)

Un réducteur (reducer) est une fonction pure, qui prend en paramètre un état et une action, pour retourner un (nouvel) état:

(état, action) => état

Décrit comment une action va modifier un état donner pour retourner un nouvel état.

Le nouvel état, retourné par la fonction, est un nouvel objet. L’état d’origine n’est pas modifié.

Cette fonction est dite pure car :

// fonction pure
const plus = (a,b) => {
  return a+b;
}

// fonction pure
const increment = (a) => {
  return a+1;
}

// fonction impure (effet de bord: fermeture)
let toto=0;
const effetDeBord = (a) => {
  toto = a;
}

// fonction impure (modifie les paramètres)
let etat = {valeur:1}
const modifieEtat = (etat) => {
  etat.valeur++;
  return etat;
}

// fonction impure (non déterministe)
const someRandomness = (etat) => {
  const valeur = Math.random();
  return {...etat, valeur}
}

// fonction pure (le paramètre n'est pas modifié)
const etat = {valeur:1}
const nouvelEtat = (etat) => {
  return Object.assign({}, etat, {valeur:etat.valeur+1} );
}

Le reducer est capable de considérer l’action en paramètre et de créer un nouvel état en fonction de cette action.

function counterReducer(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1
  case 'DECREMENT':
    return state - 1
  default:
    return state
  }
}

Le Store Redux

Le store Redux est l’objet javascript qui contient l’état immuable d’une application.

Toute modification du store doit passer par un reducer qui va générer un nouvel état.

On crée un store avec la fonction createStore et en paramètre un réducteur capable de gérer les actions.

function counterReducer(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1
  case 'DECREMENT':
    return state - 1
  default:
    return state
  }
}

let store = createStore(counterReducer)

L’objet store possède trois méthodes :

store.subscribe(() =>
  console.log(store.getState())
)

store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1

Exemple Redux de base sur CodePen.

Inclure le store dans React

Pour donner accès au store au différents composants d’une application React, Redux offre le composant Provider.

import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './components/App'
import reducer from './reducers'

const store = createStore(reducer)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

Pour qu’un composant React ait effectivement accès au store, il faut “connecter” ce dernier au store.

On utilise la fonction connect de Redux sur le composant désiré pour lui donner accès. La fonction connect retourne un nouveau composant React ayant la possibilité de

Les options de la fonction connect sont nombreuses. On peut vouloir avoir accès à la fonction dispatch uniquement ou bien s’enregistrer pour recevoir les modifications du store. Le filtrage est possible.

import React from 'react'
import { connect } from 'react-redux'

const addStuffAction = (stuff) => ({
  type: 'ADD_STUFF',
  id: Math.floor(Math.random()*1000),
  stuff
});

let AddStuff = ({ dispatch }) => {
  let input

  return (
    <div>
      <form onSubmit={e => {
        e.preventDefault()
        if (!input.value.trim()) {
          return
        }
        dispatch(addStuffAction(input.value))
        input.value = ''
      }}>
        <input ref={node => {
          input = node
        }} />
        <button type="submit">
          Add Stuff
        </button>
      </form>
    </div>
  )
}

export default connect()(AddStuff)

Classiquement on définit deux fonctions :

Exemple pris de redux.js.org

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}


import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

Combiner les réducteurs

La fonction createStore ne prend en paramètre qu’un seul réducteur qui va être chargé de gérer toutes les actions de l’application. Or plusieurs types d’actions différentes vont cohabiter.

Par exemple, les actions liées à la modification du model peuvent êtres séparées des actions liées à l’interface graphique.

On va combiner les réducteurs avec la fonction combineReducers.

Exemple

On stocke 2 types d’informations dans le store :

const stuff = (state, action) => {
  switch(action.type){
    'ADD_STUFF': return [
        ...state,
        {id: action.id, stuff: action.stuff}
      ]
    'REMOVE_STUFF': return state.filter((s)=>(s.id !== action.id))
    default: return state
  }
}

const display = (state, action) => {
  switch(action.type){
    'CHANGE_DISPLAY': return action.displayType
    default: 'DISPLAY_LIST'
  }
}

const reducer = combineReducers({
  stuff,
  display
})

const initialState = {
  stuff: [
    { id:1, stuff:'OK'},
    { id:2, stuff:'KO'}
  ],
  display: 'DISPALY_LIST'
}

const store = createStore(reducer, initialState)

Exemples Redux

On peut cloner le repo redux et étudier les exemples.

https://github.com/reactjs/redux.git ou git@github.com:reactjs/redux.git

Puis on peut explorer l’exemple TodoApp.

cd redux/examples/todos
npm i
npm start

Gestions des actions asynchrones

Redux Thunk middleware est un module redux qui permet d’écrire des fonctions de création d’actions qui retournent une fonction ou une Promise au lieu de retourner une simple action.

Cette fonction retournée reçoit les méthodes dispatch et getState du store en paramètre.

Ce mécanisme permet de retarder l’exécution du dispatch d’une action. Ce mécanisme est utile lors de l’utilisation de code asynchrone comme un appel a fetch.

On configure d’abord le store avec le middleware

import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';

const reducer = /* ... le réducteur de l'application... */

const etatInitial = {/* ... l'état initial du store... */}

const store = createStore(
  reducer,
  etatInitial,
  applyMiddleware(
    thunkMiddleware
  )
);

On peut ensuite l’utiliser dans les créateurs d’actions

function fetchStuff(lol) {
  return function (dispatch) {
    dispatch(requestStuff(lol))
    return fetch(`https://www.example.com/${lol}`)
      .then(response => response.json())
      .then(json => dispatch(receiveStuff(lol, json))
      ).catch(err => dispatch(cancelStuff(err)));
  }
}

Classiquement on utilise 3 actions intermédiaires pour une opération asynchrone (3 appels à dispatch):

Utilisation avec React-Router

Suivre le tutoriel Usage with React Router sur le site de Redux.

Pour aller plus loin