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 :
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.
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'))
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:
Date.now()
etc…Il est uniquement chargé de calculer le nextState.
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
}
}
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
Le store est un objet qui :
getState()
dispatch(action)
subscribe(listener)
(composants notifiés lorsque le state subit une modification)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.
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.
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.
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 , 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 :
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.
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
)
);
const UserList = ({ filteredUsers }) => (
<ul>
{filteredUsers.map(user => <li>{user}</li>)}
</ul>
);
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.