Reason
Reason

Reputation: 1430

Meteor, server-render withTracker. Postponing client-rendering

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]

Upvotes: 3

Views: 794

Answers (3)

Cereal
Cereal

Reputation: 3839

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.

Upvotes: 1

pravdomil
pravdomil

Reputation: 2969

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.

Upvotes: 0

xSkrappy
xSkrappy

Reputation: 747

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()
}

Upvotes: -1

Related Questions