Introduction

Dans un précédent article, nous avons vu comment implémenter de façon naïve la librairie Redux. Si vous ne l’avez pas encore lu ou que vous souhaitez simplement le relire, c’est par ici: Redux from scratch

À l’issue du tuto précédent, on avait donc un store qui mettait à jour nos composants React. On pouvait être satisfait! Seulement, on pouvait réaliser les limites de notre implémentation… En effet, chaque composant dépendant de notre store doit nécessairement l’importer et y subscriber dans la méthode componentDidMount de la façon suivante: store.subscribe(() => this.forceUpdate).

Certes, ça ne représente que deux lignes de code dans chaque fichier mais cela nous amène à écrire un peu partout du code identique en plus de lier très fortement nos composants à l’implémentation de notre store. Ce qui est problématique si à terme, on souhaite publier nos composants sur npm par exemple.

La solution existe déjà et s’appelle React Redux et nous allons l’implémenter ensemble !

Store everywhere…

Chacun des composants ayant besoin du state et des méthodes à dispatcher doit d’avoir accès d’une façon ou d’une autre au store. Il faut donc que ce dernier soit disponible PARTOUT!

Deux options s’offrent à nous :
1) Transmettre le store depuis le composant App, le composant le plus haut dans l’arborescence de l’application, en props à tous les composants enfants.
2) Utiliser le contexte

L’option numéro 1, on le réalise vite, va se révéler fastidieuse quand le composant tout en bas, bien loin en bas, de notre arborescence, aura besoin d’accéder au store. On va se retrouver un ensemble de composant qui se passeront gentiment le store jusqu’à celui qui en aura forcément besoin…

Bon j’avoue la comparaison est un peu biaisé, mais l’option 2 utilise la notion de contexte hors, le contexte dans React vient justement pour éviter les biais de l’option 1. Le contexte (un simple objet javascript) se définit au sein d’un composant et est transmis par défaut à tous les composants enfants. Cela veut dire que si nécessaire, les composants enfant pourront accéder à cet objet pour l’utiliser. Ainsi, même un le composant au bout du bout de l’arborescence pourra utiliser le contexte définit 10 niveaux de hiérarchie au dessus de lui de façon très simple.

C’est donc évidemment cette option que nous allons utiliser pour rendre notre store disponible partout dans notre application.

On va donc tenter d’écrire un composant Provider qui viendra wrapper le composant principal App qui permettra de rendre accessible le store partout dans notre application et nous fera réécrire le fichier index.js de cette façon :

1
2
3
4
5
6
7
8
9
//index.js
/* import stuff .... */
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);

Ce Provider est donc un élément React qui prend un seul props : le store si vous avez suivi et va donc le rendre disponible via le contexte aux composant enfant.

Comment fait-on cela me direz-vous ? On va y aller par étape.
Premièrement, ce composant ne gère aucun affichage en tant que tel, il se contente de transmettre de l’information aux composants enfants mais ne va en aucun cas modifier l’UI :

1
2
3
4
5
6
//provider.js
class Provider extends React.Component {
render() {
return this.props.children;
}
};

On a maintenant un composant simple qui se contente de rendre son props children. Là en l’occurence, avec ou sans Provider, rien n’a changé pour notre application si ce n’est un niveau de hiérarchie supplémentaire dans notre arbre de composants.

On va maintenant rendre Provider vraiment utile. Pour créer un contexte, il faut utiliser la méthode getChildContext, méthode qui retourne un objet qui sera le contexte auquel les composants enfants auront accès. Ici, cete méthode va retourner le store qui est passé en props à notre composant Provider.

1
2
3
4
5
6
7
8
9
10
//provider.js
class Provider extends React.Component {
//Création du context
getChildContext = () => {
return { store: this.props.store };
}
render() {
return this.props.children;
}
};

Pour que ce soit totalement complet et fonctionnel, il faut rajouter, en plus de cette méthode, le type des attributs que le contexte contient (la docs est ici ).
Ainsi, Provider va finalement s’écrire de la façon suivante:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//provider.js
class Provider extends React.Component {
//Création du context
getChildContext = () => {
return { store: this.props.store };
}
render() {
return this.props.children;
}
};
Provider.childContextTypes = {
store: PropTypes.object,
};

Done, notre composant Provider est maintenant capable de passer le store en context. le store est accessible à tous les composants enfant, VICTOIRE !!
Euh…attendez, pour le moment, on a rien simplifié puisque en l’état, il faut que l’on récupère le contexte dans tous les composants qui utilisent le store et ensuite, nous devons toujours subscriber aux mises à jour et dispatcher les actions. Finalement, on s’évite un import de fichier mais nos composant restent toujours très lié à l’implémentation de notre store, donc en l’état, on ne s’est pas encore simplifié la vie…. Comment changer ça ?

Single store looking for soul mate(s), answer please…

Ce que l’on souhaite, c’est que nos composants React soient totalement découplés du store, qu’ils n’aient pas besoin de subscriber, de connaître les actions du store ou d’appeler directement getState. On veut des composants simples, qui reçoivent des props et gèrent un rendu en conséquence, point à la ligne.

C’est ce que nous permet la fonction connect de React-Redux, et c’est cette méthode que nous allons réimplémenter. Cette fonction va nous permettre de retourner d’office un composant React couplé au store. Elle s’utilise de a façon suivante:

1
connect(mapDispatchToProps, mapStateToProps)(ComponsantQueLonSouhaiteLierAuStore);

Ouch, entre la fonction qui retourne une fonction, des noms d’arguments à rallonge mais WHAT?, c’est l’anarchie ce truc ?! Pas de panique, on va y aller en douceur :)

Ce qu’il faut avoir à l’esprit avant toute chose, c’est que l’idée de connect, c’est d’encapsuler un composant (en l’occurence dans notre exemple précédent, le bien-nommé ComponsantQueLonSouhaiteLierAuStore afin de lui fournir tous les props dont il a besoin sans pour s’afficher sans se soucier du store.

Il se contenet de récupérer les élements et de les afficher, fin.

Les 2 arguments de connect sont des fonctions et possède des noms assez explicites pour les anglophones. La fonction mapDispatchToProps (resp. mapStateToProps) prend pour argument dispatch (resp. state) et a pour objectif de retourner un objet constitué d’éléments dérivés de la fonction dispatch (resp. de l’objet state).

Pour le moment, on va s’arrêter à ça, nous verrons leur utilisation dans un cas pratique. Pour le moment, ce qui est essentiel à savoir c’est que ces deux fonctions dépendent de dispatch et state et retourne des objets.

Pour le moment, on comprend que cette méthode prend deux arguments et retourne une fonction qui prend en argument un composant React:

1
2
3
4
5
6
7
8
//connect.js
/**
imports...
**/
function(mapDispatchToProps, mapStateToProps) {
return function(Component) {
}
}

Jusqu’ici, ça va. Maintenant, qu’est-ce que l’on voulait déjà ? Ah oui, retourner le composant React en argument avec les props reçus ! Pour cela, on va enrichir un peu le code précédent :

1
2
3
4
5
6
export default function(mapDispatchToProps, mapStateToProps) {
return function(Component) {
//finalement on retourne en output de la fonction le componsant avec ses props
return <Component {...props} />;
}

Maintenant, on a bien une fonction qui retourne une fonction retournant elle-même un composant React. Sweet !
Seulement, on n’utilise même pas mapDispatchToProps et mapStateToProps. On se contente de retourner Component sans le modifier. On va donc enrichir tout ça.

Pour rappel mapDispatchToProps et mapStateToProps prennent respectivement les objets dispatch et state contenu dans notre objet store. Il faut donc accéder au store. Comment faire ? Et oui grace au contexte de l’objet Provider !

On va donc créer un composant React au sein de la fonction dont la charge sera de recupérer le contexte et de retourner dans sa méthode render, l’argument Component, l’argument de notre fonction qui aura été un peu enrichi pour l’occasion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default function(mapDispatchToProps, mapStateToProps) {
return function(Component) {
//Création d'un composant React
//qui retourne l'argument
class ConnectedComponent extends React.Component {
render() {
return <Component {...this.props} />
}
}
//pour accéder au contexte,
//on ajoute les lignes suivantes
ConnectedComponent.contextTypes = {
store: PropTypes.object,
};
//on le retourne en output de la fonction
return ConnectedComponent;
}

Maintenant, on a accès à notre store via le context. De façon identique à this.props ou this.state, on accède au context via this.context au sein d’un composant à état ou simplement context dans un composant fonctionnel.

On peut donc appeler mapDispatchToProps et mapStateToProps avec les arguments attendus.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export default function(mapDispatchToProps, mapStateToProps) {
return function(Component) {
//Création d'un composant React
//qui retourne l'argument
class ConnectedComponent extends React.Component {
render() {
//on récupère le store dans le contexte
const store = this.context.store;
//on appelle les deux fonctions qui retournent chacune un objet
const propsFromDispatch = mapDispatchToProps(store.dispatch);
const propsFromState = mapStateToProps(store.getState());
return <Component/>
}
}
//pour accéder au contexte,
//on ajoute les lignes suivantes
ConnectedComponent.contextTypes = {
store: PropTypes.object,
};
//on le retourne en output de la fonction
return ConnectedComponent;
}

Maintenant, on veux que le composant React en argument de la fonction ait en plus de ses props, les objets générés par mapDispatchToProps et mapStateToProps. On va donc enrichir la méthode render de ConnectedComponent.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//stuff avant
render() {
//on récupère le store dans le contexte
const store = this.context.store;
//on appelle les deux fonctions qui retournent chacune un objet
const propsFromDispatch = mapDispatchToProps(store.dispatch);
const propsFromState = mapStateToProps(store.getState());
return (
<Component
{...props}
{...propsFromDispatch}
{...propsFromState}
/>
);
}
//stuff après

Génial, maintenant, on retourne bien un composant qui possède des informations venant du store. Par contre, on n’oublie quelque chose, en l’état, le composant ne va pas mettre un jour son rendu lorsque le state est modifié, il faut penser à subscriber pour écouter les changements.
On obtient donc ceci :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export default function(mapDispatchToProps, mapStateToProps) {
return function(Component) {
class ConnectedComponent extends React.Component {
componentDidMount() {
//subscribe pour écouter
//les changements au niveau du state
this.context.store.subscribe(() => this.forceUpdate());
}
render() {
const {context, props} = this;
const propsFromDispatch = mapDispatchToProps(context.store.dispatch);
const propsFromState = mapStateToProps(context.store.getState());
return (
<Component
{...props}
{...propsFromDispatch}
{...propsFromState}
/>
);
}
}
ConnectedComponent.contextTypes = {
store: PropTypes.object,
};
return ConnectedComponent;
}
}

Maintenant, lorsque l’on fait :

1
2
3
function mapDispatchToProps = (dispatch) => { /** **/ }
function mapStateToProps = (state) => { /** **/ }
const connectFunction = connect(mapDispatchToProps, mapStateToProps)

Ici, connectFunction est une fonction qui attend en argument un composant React pour le lier directement au store de façon très simple.

Ainsi :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function mapDispatchToProps = (dispatch) => { onClick: () => dispatch(actionDeCliquer()) }
function mapStateToProps = (state) => { name: state.name }
const UnPetitComposant = (props) => (
<div
onClick={props.onClick}
style={{backgroundColor: props.color}}
>
{props.name}
</div>
);
const UnPetitcomposantRelieAuStore = connect(mapDispatchToProps, mapStateToProps)(UnPetitComposant)
//maintenant je peux écrire
<UnPetitcomposantRelieAuStore color={"blue"} />
//UnPetitComposant est maintenant totalement dissocié de l'implémentation du store
//il se contente de recevoir des props, du store ou non,
//et de gérer son affichage en conséquence

Place à la pratique

Pour revenir à notre exemple de boîte mail, on va pouvoir modifier de façon assez drastique le ficher principal qui ressemblait au code juste en dessous à l’isue du tutoria sur redux:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/*
imports
*/
import store from './redux/store';
import { addMail, removeMail, markMailAsRead } from './redux/actions';
class App extends React.Component {
componentDidMount() {
store.subscribe(() => this.forceUpdate());
};
hitRefreshButton = () => {
const newMails = Math.round(Math.random() * 4) + 1;
for(let i = 0; i < newMails; i++) {
store.dispatch(addMail());
};
};
render() {
const mails = store.getState().mails;
const unreadMails = mails.filter(mail => !mail.read);
//pour simplifier, je n'ai garder que
//les éléments qui nous intéressent :)
//d'ou un code bien moins long que dans le projet
return (
{ /* stuff */}
<div>
<Label >
<Icon name='mail'/> {unreadMails.length}
</Label>
</div>
<div>
<Label
as="button"
content="Refresh"
icon="refresh"
onClick={this.hitRefreshButton}
/>
</div>
{mails.length === 0 && <div style={S.centeredContainer}>
<div>No new mail, have a good day !</div>
</div>
}
{ mails.map( (mail, index) => (
<Mail
key={mail.object}
mail={mail}
index={index}
onDelete={ () => store.dispatch(removeMail(index)) }
onClickAsRead={ () => store.dispatch(markMailAsRead(index))}
/>
)
) }
);
}
}
export default App;

On voit ici qu’on appelle subscribe, dispatch ou encore getState directement au sein de notre composant. Si pour une raison X ou Y, l’implémentation de store changeait, getState devenant getCurrentState pour des raisons obscures de lisibilité ;) il faudrait changer getState dans tous les composants qui l’utilisent…pas top.

MAIS, avec connect, on peut simplifier tout ça !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/*
imports
*/
//notre fonction connect flambant neuve !!
import { connect } from "./react-redux/react-redux";
//nos actions toujours présentes
import { addMail, removeMail, markMailAsRead } from './redux/actions';
//Je pense que vous allez reconnaître ces deux bébés !
//1) les props que l'on souhaite générer avec la méthode dispatch
const mapDispatchToProps = (dispatch) => ({
addMail: () => dispatch(addMail()),
removeMail: index => dispatch(removeMail(index)),
markMailAsRead: index => dispatch(markMailAsRead(index)),
});
//2) les props que l'on souhaite générer depuis le state
const mapStateToProps = (state) => ({
mails: state.mails,
});
class App extends React.Component {
hitRefreshButton = () => {
const newMails = Math.round(Math.random() * 4) + 1;
for(let i = 0; i < newMails; i++) {
//ADIEU STORE.DISPATCH §§§
this.props.addMail();
};
};
render() {
//ici, props contient bien
//tous les éléments définis via mapDispatchToProps et mapStateToProps
const {
mails,
addMail,
removeMail,
markMailAsRead,
} = this.props;
const unreadMails = mails.filter(mail => !mail.read);
return (
{/* stuff */}
<div>
<Label
color={mails !== 0 ? 'green' : ''}
>
<Icon name='mail'/> {unreadMails.length}
</Label>
</div>
<div>
<Label
as="button"
content="Refresh"
icon="refresh"
onClick={this.hitRefreshButton}
/>
</div>
{mails.length === 0 && <div style={S.centeredContainer}>
<div>No new mail, have a good day !</div>
</div>}
{ mails.map( (mail, index) => (
<Mail
key={mail.object}
mail={mail}
index={index}
onDelete={ () => removeMail(index) }
onClickAsRead={ () => markMailAsRead(index)}
/>
))
}
);
}
}
//enfin on appelle effectivement connect avec les fonctions et on lie
//tout ça à notre composant App
export default connect(mapDispatchToProps, mapStateToProps)(App);

Et voilà, grâce à connect, notre composant App ne s’occupe que de ses props, plus aucun accès à l’objet store, ni à ses méthodes n’est nécessaire !!

###Conclusion

Avec cette découverte de react-redux, on a complété notre apprentissage de redux. Il aurait en effet été dommage de s’arrêter en si bon chemin alors que react-redux est juste une librairie indispensable dès que l’on souhaite utiliser redux dans un projet React.

Maintenant, on comprend comment et pourquoi elle existe et nous simplifie la vie puisqu’il y a bel et bien un avant et un après react-redux.
Il suffit de le voir avec un exemple aussi simple que celui que nous avons construit au cours de l’article traitant de redux

Par contre, sachez que notre version de react-redux est évidemment beaucoup, beaucoup, plus simple que le package officiel. J’ai délibérément négligé certains aspects de l’API offert par react-redux, pas à cause de la complexité mais parce qu’il ne rajouterait pas beaucoup de plus value en terme de compréhension.

Dans une toute autre mesure, react-redux soulève et résoud de nombreux problèmes très poussés de cohérence de la donnée causé notamment par l’asynchronisme de la méthode setState, de caching de component, pour répondre à des edge-cases rendant le code source pas très parlant à la première lecture… :) Pour ce qui désire approfondir le sujet, Dan Abramov parle de react-redux ici (la vidéo n’est pas de très bonne qualité malheureuseument mais le contenu oui)…

Le code détaillé dans ce poste disponible ici.

A bientôt,
Gaël