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.