3

In my database I have an array of animals that I would like to render into a nice little list. To improve the user experience, I would like to render it on the server (using the new server-render package) and then subscribe to any changes using react-meteor-data (withTracker).

Right now, this is working except for one thing. The server renders the content as expected (including the data), which is then sent to the client. The problem is on the client.

Once the page loads, meteor sets up the data connection, then renders the page. This first rendering occurs before the data connection has returned any data, so it renders an empty list of animals (overwriting the list rendered on the server and causing a warning). Then once data arrives the list is fully (re-)rendered.

This leads to a pretty bad user experience as the list blinks out and then returns. I would like to postpone the client-rendering until the data is available. Is this possible?

My code is really simple and looks like this:

List Component:

import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';

import { AnimalsData } from '../api/animals';

class Animals extends Component {
    render() {
        const {animals} = this.props;
        console.log(animals);

        return <ul>
            {animals.map(animal =>
                <li key={animal._id}>
                    {animal.name}
                </li>)
            }
        </ul>
    }
};

// Load data into props, subscribe to changes on the client
export default withTracker(params => {
    if (Meteor.isClient) {
        // No need to subscribe on server (this would cause an error)
        Meteor.subscribe('animals');
    }

    return {
        animals: AnimalsData.find({}).fetch()
    };
})(Animals);

Server:

import React from "react";
import { renderToString } from "react-dom/server";
import { onPageLoad } from "meteor/server-render";

import Animals from '../imports/ui/Animals';
import '../imports/api/animals';

onPageLoad((sink) => {
    sink.renderIntoElementById('app', renderToString(<Animals />));
});

Client:

import React from 'react';
import ReactDOM from "react-dom";
import { onPageLoad } from "meteor/server-render";

import AnimalList from '../imports/ui/Animals';

onPageLoad(sink => {
    ReactDOM.hydrate(
        <AnimalList />,
        document.getElementById("app")
    );
});

Database:

import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';

export const AnimalsData = new Mongo.Collection('animals');

if (Meteor.isServer) {

    Meteor.publish('animals', () => {
        return AnimalsData.find({});
    });
}

What happens (console.log in Animals.jsx):

  1. Renders on server [animal data]
  2. Renders on client before data arrives. This removes the list rendered on the server []
  3. Renders on the client when data arrives [animal data]
Reason
  • 1,410
  • 12
  • 33
  • take look at https://github.com/thereactivestack-legacy/meteor-react-router-ssr and https://github.com/ssrwpo/ssr – pravdomil Feb 12 '18 at 20:12
  • Thanks for your advice. I actually looked into the packages you mentioned earlier, but seeing as server-render and react-meteor-data are both packages maintained by MDG i would prefer a solution using them – Reason Feb 12 '18 at 20:38

3 Answers3

1

You can delaying hydrating your page until your subscription is ready.

For example, lets say you have a collection of links

import { Mongo } from 'meteor/mongo';
import { Meteor } from 'meteor/meteor';

export default Links = new Mongo.Collection('links');

if(Meteor.isServer) {
  Meteor.publish('links', () => {
    return Links.find({});
  });
}

In client/main.js, you would subscribe to the publication, and wait for it to be ready before continuing with your hydration. You can do that with meteor/tracker, since ready() is an observable.

import React from 'react';
import ReactDOM from 'react-dom';
import { Meteor } from 'meteor/meteor';
import { onPageLoad } from "meteor/server-render";
import App from '../imports/ui/entry_points/ClientEntryPoint';
import { Tracker } from 'meteor/tracker';

onPageLoad(async sink => {
  Tracker.autorun(computation => {
    if(Meteor.subscribe('links').ready()) {
      ReactDOM.hydrate(
        <App />,
        document.getElementById("react-target")
      );
      computation.stop();
    }
  })
});

Obviously that requires subscribing to everything in the entire app, but you could add additional logic to subscribe to different things depending on the routes.

Cereal
  • 3,699
  • 2
  • 23
  • 36
0

I have created package for your that prevents components rerender at page load.

Check it here https://github.com/pravdomil/Meteor-React-SSR-and-CSR-with-loadable-and-subscriptions.

pravdomil
  • 2,961
  • 1
  • 24
  • 38
  • Thanks for posting! It's a little unclear how I would use it. Looking through the source I see that you are overwriting Meteor.subscribe, which makes me a little nervous (since the real world case is pretty big and in production). Could you show how you would use it in the case of the animals here? – Reason Feb 14 '18 at 11:24
  • I want this solution to work - but it didn't for me. Maybe I didn't implement it correctly, but the guidance is simply too scant to know for sure. I have raised a more complete issue on the repo. thanks – Andy Lorenz Apr 26 '18 at 22:32
-1

You can use .ready() for it not to render when the subscription isn't ready yet, example:

const animalSub = Meteor.subscribe('animals')

if (animalSub.ready()) {
  return AnimalsData.find().fetch()
}
pravdomil
  • 2,961
  • 1
  • 24
  • 38
xSkrappy
  • 747
  • 5
  • 17
  • Could you elaborate on this one? The Meteor.subscribe-statement is inside the withTracker-function so the problem is that when it "does nothing" it still calls a render of the AnimalList, which is the problem I try to avoid. – Reason Feb 09 '18 at 14:42
  • it is calling since you did not wrap the return function in the ready(). you just need to wrap the return function inside the .ready , it will result that if the subscription is not yet ready , it wont render the component. I am using atm at my mobile so its hard to format the code. I guess I already gave you enough information to show how it works though – xSkrappy Feb 09 '18 at 14:43
  • Edited My answer, please see it again :) – xSkrappy Feb 09 '18 at 14:50
  • This is unfortunately not a correct solution. I'm assuming you want to put that code inside withTracker? withTracker gets called whenever animalSub is changed and once when it's initiated. What your code is doing is that it returns the correct data when it's available. When it's not available (before animalSub.ready()), withTracker still runs, but since there is no return statement, undefined is returned, which calls the re-rendering (with incorrect data) – Reason Feb 09 '18 at 15:32