Si vous n'avez pas encore lu l'introduction à flux, n'hésitez pas à jeter un œil avant de lire ce post.
Une des choses importantes avec Flux, et pourtant pas évidentes après lecture des exemples officiels, c'est que les stores doivent être des instances, et non des singletons que les composants récupèrent en dépendance directe.
La raison de cette nécessité, c'est la possibilité de servir une page pre-rendue sur le serveur. En soi, vous devez impérativement amorcer votre dispatcher et vos stores dans le scope de la requête, ou vos utilisateurs se retrouveront avec des stores remplis de data ne leur appartenant pas.
Puisque l'intérêt d'une solution comme react dans le cadre du server-side rendering est d'utiliser les mêmes composants que sur le client, il faut que notre approche soit convenable sur nos deux environements.
La question dès lors est «comment passer les stores à nos composants react maintenant qu'ils ne sont plus des dépendances directes ?».
On va les passer via les props
de parent à enfant ? lolnope. La plupart de nos
composants n'auront pas conscience de la présence de ces stores, et seront
utilisés dans différents contextes. Et c'est justement l'API context
que nous
allons utiliser.
Cette API est assez simple, en soi, le context
d'un composant est construit au
fur et à mesure que ses ancêtres décident d'y ajouter de la data.
Exemple simple :
import React, { Component, PropTypes } from "react";
class App extends Component {
// on définit les types de ce que l'on souhaite passer dans
// le contexte
static childContextTypes = {
foo: PropTypes.string,
};
// on crée une méthode qui retourne ce contexte
getChildContext() {
return {
foo: this.props.foo,
};
}
render() {
return (
<div className="putainde-App">
<Container />
</div>
);
}
}
class Container extends Component {
// les contextes sont *merged*, ce qui nous permet de le construire
// sans se soucier du niveau auquel sera notre composant.
static childContextTypes = {
bar: PropTypes.string,
};
getChildContext() {
return {
bar: "oh hai",
};
}
render() {
return (
<div className="putainde-Container">
<IntermediaryComponent />
</div>
);
}
}
// ce composant n'a pas besoin de savoir que ses enfants on besoin
// de certaines propriétés du contexte
class IntermediaryComponent extends Component {
render() {
return <Content />;
}
}
class Content extends Component {
// pour chaque composant utilisant des propriétés du contexte,
// on stipule ce dont on a besoin
static contextTypes = {
foo: PropTypes.string,
bar: PropTypes.string,
};
render() {
return (
<div className="putainde-Content">
<div className="putainde-Content-line">
<strong>foo</strong>: {this.context.foo}
</div>
<div className="putainde-Content-line">
<strong>bar</strong>: {this.context.bar}
</div>
</div>
);
}
}
React.render(<App foo={Date.now()} />, document.getElementById("App"));
Cela nous donne cet output : http://jsbin.com/zitohibaze/1/
Grâce à cette API, on peut créer des composants isolés, et dont les composants parents n'auront pas nécessairement besoin de connaître le contexte.
L'idée, pour en revenir à Flux, c'est de passer notre dispatcher dans ce contexte, et de placer les stores dans le dispatcher au moment de l'amorce de l'app.
const dispatcher = new Dispatcher();
dispatcher.registerStore(new SomeStore());
dispatcher.registerStore(new SomeOtherStore());
React.render(<App dispatcher={dispatcher} />, document.getElementById("App"));
Désormais, pour avoir une API décente pour récuperer les données des stores, on a deux principales solutions:
Puisque la direction que prend l'API de React, à terme, est de ne plus fournir de mixins, et de laisser au TC39 le temps de prendre la bonne décision sur la façon dont JavaScript traitera la composition ; il semble plus adéquat d'utiliser un higher-order component. Cela aura en plus l'avantage de rendre le composant récupérant les données stateless.
Ce genre d'API ressemble à ça :
class ComponentWithData extends Component {
static stores = {
// nom du store: nom de la prop souhaitée
MyStore: "my_store",
};
render() {
return (
<div>
{/* la data est passée via les props*/}
{this.props.my_store.foo}
</div>
);
}
}
// storeReceiver wrap `ComponentWithData` dans un higher-order component
// et se charge de récupérer le store dans le contexte pour les passer
// dans les props de `ComponentWithData`
export default storeReceiver(ComponentWithData);
Enfin, avec cette approche, les action creators que l'on voit dans les exemples de flux ne peuvent plus garder la même forme, puisqu'il ne doivent plus avoir le dispatcher comme dépendance directe (ce dernier étant une instance). Du coup, ce sont désormais des fonctions pures :
const PostActions = {
getPost(slug) {
return {
type: ActionTypes.POST_GET,
slug: slug,
};
},
receivePost(res) {
return {
type: ActionTypes.POST_RECEIVE,
res,
};
},
error(res) {
return {
type: ActionTypes.POST_ERROR,
res,
};
},
};
et nos composants utilisent :
import React, { Component, PropTypes } from "react";
import PostActions from "actions/PostActions";
class MyComponent extends Component {
static contextTypes = {
dispatcher: PropTypes.object,
};
static propTypes = {
slug: PropTypes.string,
title: PropTypes.title,
};
handleClick() {
this.context.dispatcher.dispatch(PostActions.getPost(this.props.slug));
}
render() {
return (
<button onClick={() => this.handleClick()}>{this.props.title}</button>
);
}
}
Pour résumer, les avantages de cette approche sont :
Bisous bisous.