web-dev-qa-db-fra.com

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

Un des plus gros avantages de React.js est supposé être le rendu côté serveur. Le problème est que la fonction clé React.renderComponentToString() est synchrone, ce qui rend impossible le chargement de données asynchrones car la hiérarchie des composants est restituée 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 propriété, une sorte d'identifiant (par exemple l'id d'un article sous les commentaires), et tout le reste est géré par le composant lui-même (chargement, ajout, gestion des commentaires). 

J'aime beaucoup l'architecture Flux car elle facilite beaucoup de choses et ses magasins sont parfaits pour le partage d'état entre serveur et client. Une fois que ma boutique contenant les commentaires est initialisée, je peux simplement la sérialiser et l’envoyer du serveur au client où elle est facilement restaurée.

La question est de savoir quel est le meilleur moyen de peupler mon magasin. Au cours des derniers jours, j'ai beaucoup cherché sur Google et je n'ai rencontré que peu de stratégies, aucune d'entre elles ne semblant vraiment bonne compte tenu du degré de "promotion" de cette fonctionnalité de React.

  1. À mon avis, le moyen le plus simple est de peupler tous mes magasins avant le début du rendu. Cela signifie quelque part en dehors de la hiérarchie des composants (connecté à mon routeur par exemple). Le problème avec cette approche est que je devrais à peu près définir deux fois la structure de la page. Prenons une page plus complexe, par exemple une page de blog avec de nombreux composants différents (publication de blog, commentaires, publications associées, publications les plus récentes, flux Twitter, etc.). Je devrais concevoir la structure de la page à l'aide de composants React, puis ailleurs, je devrais définir le processus de remplissage de chaque magasin requis pour cette page en cours. Cela ne me semble pas être une bonne solution. Malheureusement, la plupart des didacticiels isomorphes sont conçus de cette façon (par exemple, cet excellent flux-tutorial ).

  2. React-async . Cette approche est parfaite. Cela me permet simplement de définir dans une fonction spéciale de chaque composant comment initialiser l'état (que cela soit synchrone ou asynchrone) et ces fonctions sont appelées lorsque la hiérarchie est rendue au format HTML. Cela fonctionne de manière à ce qu'un composant ne soit pas rendu tant que l'état n'est pas complètement initialisé. Le problème est que cela nécessite Fibres qui, pour autant que je sache, est une extension Node.js qui modifie le comportement JavaScript standard. Bien que j'aime beaucoup le résultat, il me semble toujours 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 faire cela pour utiliser cette fonctionnalité essentielle de React.js. Je ne suis pas sûr non plus du support général de cette solution. Est-il possible d’utiliser Fibre sur un hébergement Web standard Node.js?

  3. Je pensais un peu par moi-même. Je n'ai pas vraiment réfléchi aux détails d'implémentation, mais l'idée générale est d'étendre les composants de la même manière que React-async, puis d'appeler à plusieurs reprises React.renderComponentToString () sur le composant racine. À chaque passage, je récupérais les rappels étendus, puis je les appelais à la fin du laissez-passer pour remplir les magasins. Je répéterais cette étape jusqu'à ce que tous les magasins requis par la hiérarchie de composants actuelle soient remplis. Il y a beaucoup de choses à résoudre et je suis particulièrement incertain de la performance. 

Ai-je manqué quelque chose? Y a-t-il une autre approche/solution? À l'heure actuelle, je songe à adopter la méthode réactif-asynchrone/fibres, mais je ne suis pas tout à fait sûr à ce sujet, 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 de React sont destinés à être utilisés. Vous aimez une simple couche de vue (à peu près ma suggestion numéro un) ou de vrais composants indépendants et autonomes?

110
tobik

Si vous utilisez react-router , vous pouvez simplement définir une méthode willTransitionTo dans les composants, à laquelle est transmis un objet Transition sur lequel vous pouvez appeler .wait on.

Peu importe si renderToString est synchrone car le rappel à Router.run ne sera appelé que lorsque toutes les promesses de .waited seront résolues. Ainsi, lorsque renderToString sera appelé dans le middleware, vous auriez pu remplir les magasins. 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.renderToString(<Handler routerState={state} />);
            var locals = {markup: markup};
            res.render("layouts/main", locals);
        });
    };
};

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

14
Esailija

Cela m’a beaucoup dérangé aujourd’hui, et bien que cela ne soit pas une réponse à votre problème, j’ai utilisé cette approche. Je souhaitais utiliser Express pour le routage plutôt que React Router, et je ne voulais pas utiliser de fibres, car je n'avais pas besoin de la prise en charge du threading dans les nœuds.

Je viens donc de décider que, pour les données initiales devant être restituées au magasin de flux en charge, je vais exécuter une demande AJAX et transmettre les données initiales au magasin.

J'utilisais Fluxxor pour cet exemple.

Donc, sur mon itinéraire express, dans ce cas, un itinéraire /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));

Ensuite, ma méthode de rendu initialize 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 flux = new Fluxxor.Flux(stores, actions);

  var App = React.createClass({
    render: function() {
      return (
          <Product flux={flux} />
      );
    }
  });

  var ProductApp = React.createFactory(App);
  var html = React.renderToString(ProductApp());
  // using ejs for templating here, could use something else
  res.render('product-view.ejs', { app: html });
0
steven iseki

Je sais que cette question a été posée il y a un an, mais nous avions le même problème et nous le résolvons avec des promesses imbriquées dérivées des composants à rendre. À la fin, nous avions 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 (
            <label>
                <Header data={this.props.header} />
                <Feed data={this.props.feed}/>
                <Footer data={this.props.footer} />
            </label>
        );

    }

});

et dans le routeur

var AppFactory = React.createFactory(App);

App.getData(t, user).then(
    /**
     *
     * @param data
     */
    function (data) {

        var app = React.renderToString(
            AppFactory(data)
        );       

        res.render(
            'layout',
            {
                body: app,
                someData: JSON.stringify(data)                
            }
        );

    }
).fail(
    /**
     *
     * @param error
     */
    function (error) {
        next(error);
    }
);
0
Rotem

juste comme un petit résumé -> GraphQL résoudra ceci entièrement pour votre pile ...

  • ajouter GraphQL
  • utiliser apollo et react-apollo
  • utilisez "getDataFromTree" avant de commencer le rendu

-> getDataFromTree trouvera automatiquement toutes les requêtes impliquées dans votre application et les exécutera, déplaçant votre cache apollo sur le serveur et permettant ainsi le fonctionnement complet de la SSR.

0
jebbie

Je sais que ce n’est probablement pas exactement ce que vous voulez et que cela n’a peut-être pas de sens, mais je me souviens d’avoir légèrement modifié le composant pour gérer les deux:

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

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 (
      <div>
        {this.state.username}'s last Gist is
        <a href={this.state.lastGistUrl}>here</a>.
      </div>
    );
  }
});

// On the client side
React.renderComponent(
  <UserGist source="https://api.github.com/users/octocat/gists" />,
  mountNode
);

// On the server side
getTheInitialState().then(function (initialState) {

    var renderingOptions = {
        initialState : initialState;
        serverSide : true;
    };
    var str = Xxx.renderComponentAsString( ... renderingOptions ...)  

});

Je suis désolé, je n'ai pas 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 plupart des composants comme une vue muette et de récupérer autant que possible des données out du composant.

0
phtrivier

Je veux partager avec vous mon approche du rendu côté serveur en utilisant Flux, peu simplifiée par exemple:

  1. Disons que nous avons 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 pour 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 <MyComponent/>;
    });
    
0
zooblin