How to initialize app state with React + SSR?

I’m new to Meteor and SSR, and I’ve had a hard time finding information on how to properly initialize app data on the first render when doing SSR. To be clear, a page in my application needs some data, and I want that data sent from my server when the page is first requested.

Is there any documentation that I’m overlooking?

Also, would it be a best practice to wrap withTracker code in a conditional so that it doesn’t run on the server?

Any guidance is appreciated.

This can be complex if you are using mongo/minimongo/tracker (Apollo has more info about how to do this available). That said - I did recently work out a great solution! (If you’d like to simply get to some helpful code, check out my starter repo).

Here were my requirements:

  • For scale and performance, don’t use pub/sub - use methods to grab and deliver data instead. This is not strictly necessary, but I’ve found pub/sub to be slow, even just for a single user in many cases.
  • Avoid any work in SSR if user is logged in - makes a lot of things easier. We only handle static routes - authenticated routes are not static.
  • Use offline storage, instead of temporary in-memory storage (the default for pub/sub).

To do this, I set a couple of rules:

  • We’ll use mongo queries on the server and client, in an isomorphic way.
  • On the server, during SSR, we’ll capture all query data, and output it as JSON (actually, EJSON) for hydration to use.
  • On the server we’ll use mongo queries directly, instead of first waiting on data from a method call (or subscription).
  • The same goes for first-render after hydration on the client - no need to wait for a server round trip, since we already got the data. Instead, hydrate the data, then render the react tree.

All of this takes a lot of coordination. There are several steps to accomplish on the server, then a similar amount of different steps to accomplish on the client. What I did was create an abstraction to hide that complexity (you can find descriptions of my earlier attempts which use withTracker in this forum). My earlier attempts ended up hitting the database twice on the server, and then again after hydration - not very efficient. I also had to coordinate manually created server-methods, with manually invoked method calls, and make sure all the pagination data was passed along, while not losing my query specific data. When I recently ported my solution to hooks, I used the opportunity to make my abstraction even better!

Here’s a high level description of the process:

I use a set of “connectors”. These are like re-usable preconfigured ‘withTracker’ HOCs, which were not tied to a specific component, but had a consistent data delivery API (always had isLoading and data props), and could be reused all over the app. On the server, they’d query the database directly, and set up the glue to capture the data for serialization, using a Provider, and an internal Consumer. On the client The data would be driven out of the ground:db for offline data - this way if there’s data already in offline, it would just use that immediately. It would query the server, and do necessary syncing once it got data from the query (while making sure to skip this if doing first-pass “hydration”).

I designed that API to basically take two different methods - the “tracked” method, and the method which would call the server method to get the data. This was more open ended, because the tracker method could have anything reactive in it, but it was also hard to optimize.

My new solution is built on hooks. Since hooks are more micro in their nature, it’s the perfect opportunity to really dial in to the specific requirements of my use case. Here’s what the API looks like (it uses validated methods behind the scenes, and the API is modeled on that):


export const useMyTiles = createListHook({
  name: 'myTiles',
  collection: Tiles,
  validate () {
    if (!Meteor.userId()) {
      throw new Meteor.Error('access-denied', 'Access Denied')
    }
  },
  query: () => ({ owner: Meteor.isClient && Meteor.userId() })
})


export const useGroupTiles = createListHook({
  name: 'groupTiles',
  collection: Tiles,
  validate () {},
  query: ({ groupId }) => ({
    $and: [
      { groupId },
      getAccessQuery()
    ]
  })
})

That’s it! Using this, along with createListHook, it sets up everything I need on the server, and on the client to do offline-first, data-over-methods, with pagination, and SSR, with data hydration, etc. (along with using a set of providers in SSR and hydration code). Super spiffy! In use, it looks like this:

// Here we use the group tiles with its consistent props contract built off the name prop
const GroupFeedPage = (props) => {
  const { groupTiles, groupTilesAreLoading } = useGroupTiles(props)
  return <FeedPage tiles={groupTiles} isLoading={groupTilesAreLoading} feedTileClassName={props.feedTileClassName} />
}

// PagedFeed is a generic component which handles pagination and infinite scrolling
const GroupFeed = ({ groupId }) => (
  <PagedFeed FeedPageComponent={GroupFeedPage} feedPageProps={{ groupId }} />
)

The implementation still needs some work to optimize. Specifically, there are some things I can do to optimize the syncing process, and reduce the data flow overhead back and forth, etc. But this is has been pretty easy to work with. There are even some fun things we can do here - maybe I can add offline and pub props for example, to allow this to either disable offline support, or to enable Meteor’s pub/sub (in those cases in my app, since I don’t server-render auth-routes, I’ve simply avoided this stack, but build similar hooks - that works well with the new useTracker method).

I’m thinking of making the data tier of this into an open source package. I have to clear a few things before I can share that, and will try to do so soon.

You can find some of the SSR stuff here (without the data part for now), in my starter repo.

5 Likes

@captainn Thanks for the detailed response! It was helpful, and your createListHook solution looks elegant. I can definitely see that being a useful library for others. Is the implementation published anywhere?

It’s not published yet, but I hope to get it up soon as a package. I just have to work out some licensing issues. When I get it up I’ll update my starter to show how to leverage it.

2 Likes

if you use collection, you can use https://github.com/abecks/meteor-fast-render

which hydrates collections on the client. Should also work with withTracker i guess.

and as far as i know, you don’t have to conditionlly exclude withTracker on the server as Tracker code just runs once

@captainn Awesome! I look forward to trying that out.

@macrozone Thanks, I’ll check that lib out.

Re: withTracker, I may have something misconfigured, but while I was toying with SSR, I added some logs to one of my withTracker methods and noticed that it did run on the server and on the client. This made sense to me since it was being imported in both places. In my withTracker code, I was subscribing to and fetching data from a collection. My concern was that the response from the server would be somewhat slower because data was being fetched on the server but then thrown away (I hadn’t figured out how to send it to the client at that point)

if you do collection.find on the server, it will make a database call directly and fetch all data that is in the database regardless of any subscriptions. This can result in a different dataset then on the client, so you have to take special care that you use selectors that match exactly your publications.

This is one big problem of the pub/sub mechanism.

You don’t have to exclude withTracker, but there are a number of things to be aware of:

  • You should not use or even try to access Meteor.user() in SSR code, as it’ll throw an error (meteor-fast-render might solve that).
  • If you have loading (waiting for subscription) in your client side app, you’ll want to skip waiting for that in SSR code, and just query the database.
  • On the client side, if you have hydration data, you’ll also want to skip waiting for the subscription in your first pass through the react tree - or wait for the subscription before hydrating the react tree, which may be tricky, given that data hydration is triggered from within the react tree!

meteor-fast-render is a fork of kadira:fast-render - is it still tied closely to FlowRouter?

This problem can be mitigated by capturing exactly what data is requested during SSR on the server, serializing it to deliver with pre-rendered HTML, then hydrating it client side, before the react tree is hydrated.

i think fast-render should work without flowrouter and yes, it allows to use Meteor.user() on SSR.
It also skips waiting for subscriptions. Instead, it gathers all Meteor.subscribe calls and passes that as initial cache to the client, so that all subscriptions on the client are ready immediatly.