Stratégies de rendu côté serveur des composants React.js initialisés de manière asynchrone

L’un des principaux avantages de React.js est censé être le rendu côté serveur . Le problème est que la fonction clé React.renderComponentToSsortingng() est synchrone, ce qui rend impossible le chargement de données asynchrones lors du rendu de la hiérarchie des composants sur le serveur.

Disons que j’ai un composant universel pour commenter que je peux déposer à peu près n’importe où sur la page. Il n’a qu’une seule propriété, une sorte d’identifiant (par exemple, l’id d’un article sous lequel les commentaires sont placés), et tout le rest est géré par le composant lui-même (chargement, ajout, gestion des commentaires).

J’aime beaucoup l’architecture Flux car cela facilite beaucoup les choses et ses magasins sont parfaits pour partager l’état entre le serveur et le client. Une fois mon magasin contenant les commentaires initialisé, je peux simplement le sérialiser et l’envoyer du serveur au client où il est facilement restauré.

La question est de savoir quelle est la meilleure façon de peupler mon magasin. Au cours des derniers jours, j’ai beaucoup cherché sur Google et j’ai rencontré peu de stratégies, dont aucune ne semblait vraiment bonne compte tenu de l’importance de la promotion de cette fonctionnalité de React.

  1. À mon avis, le plus simple est de remplir tous mes magasins avant que le rendu ne commence. Cela signifie quelque part en dehors de la hiérarchie des composants (accroché à mon routeur par exemple). Le problème avec cette approche est que je devrais définir deux fois la structure de la page. Considérez une page plus complexe, par exemple une page de blog avec de nombreux composants différents (article de blog, commentaires, articles connexes, articles les plus récents, stream Twitter, etc.). Je devrais concevoir la structure de la page en utilisant des composants React et ensuite, quelque part, je devrais définir le processus de remplissage de chaque magasin requirejs pour cette page en cours. Cela ne semble pas être une bonne solution pour moi. Malheureusement, la plupart des didacticiels isomorphes sont conçus de cette manière (par exemple, ce didacticiel sur le stream ).

  2. React-async . Cette approche est parfaite. Il me permet simplement de définir dans une fonction spéciale de chaque composant comment initialiser l’état (peu importe si celui-ci est synchrone ou asynchrone) et ces fonctions sont appelées au fur et à mesure que la hiérarchie est rendue au format HTML. Cela fonctionne de manière à ce qu’un composant ne soit pas rendu jusqu’à ce que l’état soit complètement initialisé. Le problème est que cela nécessite des fibres qui, pour autant que je sache, une extension Node.js qui modifie le comportement standard de JavaScript. Bien que j’aime vraiment le résultat, il me semble encore qu’au lieu de trouver une solution, nous avons changé les règles du jeu. Et je pense que nous ne devrions pas être obligés de le faire pour utiliser cette fonctionnalité principale de React.js. Je ne suis pas certain non plus du support général de cette solution. Est-il possible d’utiliser Fiber sur l’hébergement Web standard de Node.js?

  3. Je pensais un peu par moi-même. Je n’ai pas vraiment réfléchi aux détails de l’implémentation, mais l’idée générale est d’étendre les composants de manière similaire à React-async, puis d’appeler plusieurs fois React.renderComponentToSsortingng () sur le composant racine. Au cours de chaque passage, je collectais les rappels en extension, puis les appelais au et de la passe pour remplir les magasins. Je répéterais cette étape jusqu’à ce que tous les magasins requirejs par la hiérarchie des composants actuels soient remplis. Il y a beaucoup de choses à résoudre et je ne suis pas certain de la performance.

Ai-je manqué quelque chose? Y a-t-il une autre approche / solution? Pour le moment, je pense à la méthode asynchrone / fibres, mais je ne suis pas tout à fait sûr, comme expliqué dans le deuxième point.

Discussion connexe sur GitHub . Apparemment, il n’y a pas d’approche officielle ni même de solution. La vraie question est peut-être de savoir comment les composants React sont destinés à être utilisés. Comme une simple couche de visualisation (à peu près ma première suggestion) ou comme de véritables composants indépendants et autonomes?

Si vous utilisez react-router , vous pouvez simplement définir une méthode willTransitionTo dans les composants, qui reçoit un object Transition que vous pouvez appeler .wait .

Peu importe que renderToSsortingng soit synchrone car le rappel de Router.run ne sera pas appelé tant que toutes les promesses de .wait ed ne seront pas résolues, vous auriez pu renseigner les magasins au moment où vous renderToSsortingng . Même si les magasins sont des singletons, vous pouvez simplement définir leurs données temporairement juste à temps avant l’appel de rendu synchrone et le composant le verra.

Exemple de middleware:

 var Router = require('react-router'); var React = require("react"); var url = require("fast-url-parser"); module.exports = function(routes) { return function(req, res, next) { var path = url.parse(req.url).pathname; if (/^\/?api/i.test(path)) { return next(); } Router.run(routes, path, function(Handler, state) { var markup = React.renderToSsortingng(); var locals = {markup: markup}; res.render("layouts/main", locals); }); }; }; 

L’object routes (qui décrit la hiérarchie des routes) est partagé textuellement avec le client et le serveur

Je sais que ce n’est probablement pas exactement ce que vous voulez, et cela pourrait ne pas avoir de sens, mais je me souviens d’avoir réussi à modifier légèrement le composant pour gérer les deux:

  • le rendu côté serveur, avec tout l’état initial déjà récupéré, de manière asynchrone si nécessaire)
  • rendu du côté client, avec ajax si nécessaire

Donc quelque chose comme:

 /** @jsx React.DOM */ var UserGist = React.createClass({ getInitialState: function() { if (this.props.serverSide) { return this.props.initialState; } else { return { username: '', lastGistUrl: '' }; } }, componentDidMount: function() { if (!this.props.serverSide) { $.get(this.props.source, function(result) { var lastGist = result[0]; if (this.isMounted()) { this.setState({ username: lastGist.owner.login, lastGistUrl: lastGist.html_url }); } }.bind(this)); } }, render: function() { return ( 
{this.state.username}'s last gist is here.
); } }); // On the client side React.renderComponent( , mountNode ); // On the server side getTheInitialState().then(function (initialState) { var renderingOptions = { initialState : initialState; serverSide : true; }; var str = Xxx.renderComponentAsSsortingng( ... renderingOptions ...) });

Je suis désolé de ne pas avoir le code exact sous la main, donc cela pourrait ne pas fonctionner immédiatement, mais je poste dans l’intérêt de la discussion.

Là encore, l’idée est de traiter la majeure partie du composant comme une vue idiote et de traiter autant que possible l’extraction des données hors du composant.

J’ai été vraiment décontenancé avec cela aujourd’hui et bien que ce ne soit pas une réponse à votre problème, j’ai utilisé cette approche. Je voulais utiliser Express pour le routage plutôt que React Router, et je ne voulais pas utiliser les Fibres car je n’avais pas besoin de la gestion des threads dans node.

Je viens donc de prendre la décision que pour les données initiales devant être rendues au magasin de stream en charge, je vais effectuer une requête AJAX et transmettre les données initiales au magasin.

J’utilisais Fluxxor pour cet exemple.

Donc, sur mon itinéraire express, dans ce cas une route /products :

 var request = require('superagent'); var url = 'http://myendpoint/api/product?category=FI'; request .get(url) .end(function(err, response){ if (response.ok) { render(res, response.body); } else { render(res, 'error getting initial product data'); } }.bind(this)); 

Puis ma méthode de rendu initialise qui transmet les données au magasin.

 var render = function (res, products) { var stores = { productStore: new productStore({category: category, products: products }), categoryStore: new categoryStore() }; var actions = { productActions: productActions, categoryActions: categoryActions }; var stream = new Fluxxor.Flux(stores, actions); var App = React.createClass({ render: function() { return (  ); } }); var ProductApp = React.createFactory(App); var html = React.renderToSsortingng(ProductApp()); // using ejs for templating here, could use something else res.render('product-view.ejs', { app: html }); 

Je sais que cette question a été posée il y a un an mais nous avons eu le même problème et nous le résolvons avec des promesses nestedes dérivées des composants qui vont être rendus. En fin de compte, nous avons eu toutes les données pour l’application et nous les avons simplement envoyées.

Par exemple:

 var App = React.createClass({ /** * */ statics: { /** * * @returns {*} */ getData: function (t, user) { return Q.all([ Feed.getData(t), Header.getData(user), Footer.getData() ]).spread( /** * * @param feedData * @param headerData * @param footerData */ function (feedData, headerData, footerData) { return { header: headerData, feed: feedData, footer: footerData } }); } }, /** * * @returns {XML} */ render: function () { return (  ); } }); 

et dans le routeur

 var AppFactory = React.createFactory(App); App.getData(t, user).then( /** * * @param data */ function (data) { var app = React.renderToSsortingng( AppFactory(data) ); res.render( 'layout', { body: app, someData: JSON.ssortingngify(data) } ); } ).fail( /** * * @param error */ function (error) { next(error); } ); 

Vous souhaitez partager avec vous mon approche du rendu côté serveur en utilisant Flux , peu de choses sont simplifiées par exemple:

  1. Disons que nous avons un component avec les données initiales du magasin:

     class MyComponent extends Component { constructor(props) { super(props); this.state = { data: myStore.getData() }; } } 
  2. Si la classe nécessite des données préchargées pour l’état initial, créons Loader for MyComponent :

      class MyComponentLoader { constructor() { myStore.addChangeListener(this.onFetch); } load() { return new Promise((resolve, reject) => { this.resolve = resolve; myActions.getInitialData(); }); } onFetch = () => this.resolve(data); } 
  3. Le magasin:

     class MyStore extends StoreBase { constructor() { switch(action => { case 'GET_INITIAL_DATA': this.yourFetchFunction() .then(response => { this.data = response; this.emitChange(); }); break; } getData = () => this.data; } 
  4. Maintenant, chargez simplement les données dans le routeur:

     on('/my-route', async () => { await new MyComponentLoader().load(); return ; });