React SSR data hydration help

I’m using meteor-react-router-ssr and have SSR working well, but I need some help on how to get data hydration working, with a reactive data source.

I have some routes/components which display a reactive data source from a Meteor.subscribe followed by collection.find(). I need the components to be reactive so that changes made on one client are updated on other clients.

The components are rendered on the server perfectly with the correct data, but then they disappear and reappear quickly once the client-side subscribe and find functions kick in and replace the SSR elements with reactive elements. (I also get the console warning “React attempted to reuse markup in a container but the checksum was invalid....”)

meteor-react-router-ssr has dehydrateHook and rehydrateHook, but can some kind person explain how to use these with meteor subscriptions?

Edit: missed the important point: On the client, if the route is navigated to from another page (i.e. no SSR), then to prevent the subscription data being displayed before the subscription is complete, I am conditionally showing the element only when subscription.ready() is true. So the server renders the complete component, then the client removes it and renders it again when the subscription is ready. This is what’s causing the flash and console error.

Is there a way to prevent the list from displaying until the subscription is ready unless it has already been rendered by the server, or, ideally, hydrating the SSR with a reactive data source so that the subscribe on the client is instant and doesn’t need another round-trip to the server?

Thanks!

1 Like

Since no one has proposed a solution I thought I’d post the solution I came up with. The meteor-react-router-ssr dehydrateHook and rehydrateHook only have examples for Redux, and I couldn’t figure out how to use them with Meteor pub/sub. After much googling and trial and error I came up with this solution:

(Note that I am using the awesome https://viewmodel.org/react, many thanks to @manuel, but you should get the idea)

Note the script tag at the end of the component where the ssr stores the hydrated raw data for the client to pick up and render while it waits for it’s own subscription to be ready. This eliminates the flash.

TeamList({
    teamId: this.props.params.teamId,
    dataHydration: '',
    users: [],
    subUsersInTeam: null,

    created() {
        if (Meteor.isServer) {
            // subscribe to the data, note that on the sever the subscription is ready immediately
            Meteor.subscribe('users_in_team', this.teamId());

            // put the user subscription in this.users so the component can be pre-rendered properly
            this.users(Meteor.users.find({teams:this.teamId()}));

            // also put the raw data in the output so it can be picked up by the client
            // to be used until the client's own subscription is ready
            this.dataHydration(JSON.stringify(this.users().fetch()));
        } else {
            // pick up the hydrated raw data and use this for the initial client render while waiting for own subscription
            // Note that hydrated data will only exist if this component is on the initial render.  
            // It will not exist if the client navigates to this component from a different route.
            if ($("#hydration").text()!='') this.users(JSON.parse($("#hydration").text()));
        }
    },
    autorun() { // this only runs on client
        // stop old subscription if teamId changes
        // use .value so it is non-reactive, because we're about to change it and don't want infinite loop
        this.subUsersInTeam.value && this.subUsersInTeam.value.stop();  

        // subscribe to reactive data source, and store the subscription handle.
        // note that since we're in an autorun the subscription is stopped automatically
        // when the component is destroyed
        this.subUsersInTeam(Meteor.subscribe('users_in_team', this.teamId(), () => {
            // replace the previous data with the new subscription, once the data is ready
            this.users(Meteor.users.find({teams:this.teamId()}));
        }));
    },
    componentDidUpdate() {
        this.teamId(this.props.params.teamId);
    },
    render() {
        <div>
            <TeamListUsers teamId={this.teamId()} users={this.users()}/>
            <script dangerouslySetInnerHTML={{__html: this.dataHydration()}} id="hydration" />
        </div>
    }
})

I hope someone finds this useful, and I’m happy to hear of any better solutions!

2 Likes

How do you get Meteor.subscribe to work when Meteor.isServer?

meteor-react-router-ssr creates a fake Meteor.subscribe function on the server.

I see. I researching how to use SSR with react. I ended up using the official server-render (instead of meteor-react-router-ssr) and have to handle (not handle) subscriptions on the server to get the SSR to work.

Since you don’t need the subscription on the server (because the complete collections are already available), you could either prefix any Meteor.subscribe with if (Meteor.isClient), or you could create your own fake Meteor.subscribe function on the server. You also need to make sure any collection.find() calls are well written to return only data which would normally be subscribed to.

1 Like

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

I jus updated Fast Render to add automatic support for hydration FastRender 3.0 is here! Now with SSR data hydration helpers

1 Like

you don’t - as in you don’t need to - you already have access to any/all data you need directly from your mongo data source! So you can either:
a) if (Meteor.isClient) { Meteor.subscribe('subscription-name'); }
b) const ready = Meteor.isClient ? Meteor.subscribe('subscription-name').ready() : true;
c) define (overwrite) Meteor.subcribe function on the server to return a dummy subscription object.
I used a/b (b is for when you want to track the status of the subscription)

created separate topic