Meteor.user in SSR (onPageLoad)

For most of my app I avoid trying to render authenticated routes with SSR. I figure if the user is committed enough to use the app, they’ll stick around with slightly longer refresh times for authenticated routes, and bots aren’t authenticated anyway. But there are a few edge cases where I want to sprinkle logged-in touches here and there, and without Meteor.userId and Meteor.user available during SSR, I get differing HTML on the client - which breaks hydration.

Basically, we need Meteor.user during hydration. Anyone know of a way to get it?

I think if I call a Method from inside onPageLoad, that method will have access to this information - I’m going to try and grab the info that way, then override the built-ins, and see if it works. If anyone has figured this out yet, I’d love a pointer or two! Update: This didn’t work -in retrospect, that makes sense, since the server doesn’t have access to the security token. I assume when a method is invoked from the client app it sends that. That must be why fast-render sends it along with a cookie. I’ll look into using a technique like that.

If I understood correctly, you’re looking for something like that?

Basically the cookie is used to grab the user info from the DB then the user object can be passed along with the page to the client.

The next pieces I need are where to set the cookie, and how to override the built-in Meteor.user and Meteor.userId.

This should be a built in feature for server-render and accounts-base.

1 Like

I agree, it should be built-in or package.

For the token, I’ve used the code from the fast-render package. I didn’t override the Meteor.user I just created helper function that retrieves the SSRed user doc if available. So basically it check if Meteor.user() is defined, if not then it tries to fetch it from the SSRed doc.

We could have a better package for that, this feels like express not Meteor!

https://github.com/abecks/meteor-fast-render has an implementation for this. It uses the same API as server-render, but can SSR and hydrate for you. Works with pub-sub, handles the cookie/auth, has server side stubs for client APIs. It works nicely. It is a bit heavy since there is quite a bit of legacy code from the older fast render package that all ends up in the initial bundle, but it’s the only way I have been able to reliably get User state to work with SSR.

In rethinking this problem, the truth is, I don’t really need SSR for authenticated users at all. The content they receive is individualized, so why bother running it on the server. All I really need is to avoid rendering anything on the server for authenticated users.

So, what if I simply set a cookie with a boolean value - is logged in, or not. If the cookie is set, avoid SSR, if not set, render the non-logged in content. I think I’ll probably do this.

I did that - it’s interesting how much longer it takes for the page to load haha.

Here’s what I did on the client:

import { Meteor } from 'meteor/meteor'

// Listen to changes in auth status. If not logged in, remove auth cookie, otherwise reset it
Meteor.startup(() => {
  resetToken()
})

// override Meteor._localStorage methods and resetToken accordingly
const originalSetItem = Meteor._localStorage.setItem
Meteor._localStorage.setItem = (key, value) => {
  if (key === 'Meteor.loginToken') {
    Meteor.defer(resetToken)
  }
  originalSetItem.call(Meteor._localStorage, key, value)
}

const originalRemoveItem = Meteor._localStorage.removeItem
Meteor._localStorage.removeItem = (key) => {
  if (key === 'Meteor.loginToken') {
    Meteor.defer(resetToken)
  }
  originalRemoveItem.call(Meteor._localStorage, key)
}

function resetToken () {
  const loginToken = Meteor._localStorage.getItem('Meteor.loginToken')
  const loginTokenExpires = new Date(Meteor._localStorage.getItem('Meteor.loginTokenExpires'))

  if (loginToken) {
    setCookie('sssr', 1, loginTokenExpires)
  } else {
    setCookie('sssr', '', -1)
  }
}

function setCookie (name, value, date) {
  const expires = date === -1
    ? 'expires=Thu, 01 Jan 1970 00:00:01 GMT'
    : 'expires= ' + date.toUTCString()
  document.cookie = name + '=' + value + ';' + expires + ';path=/'
}

And the server, I simply check for any truthy value on that cookie:

onPageLoad(async sink => {
  // if cookie sssr (skip server side rendering) is set, don't bother with ssr
  if (sink.request.cookies.sssr) return
  // ... do the rest of SSR
}
4 Likes

Dang! I was genuinely curious to see what approach you would come up with to SSR and hydrate the user!

Same here haha @captainn surely has his ways!

I probably will still look into ways to hydrate the user - but I couldn’t come up with anything that wasn’t either hacky or made changes to core. If I really needed to (the hacky way), I would probably just override Meteor.user() and Meteor.userId() similar to the fast-render approach to overriding Meteor._localStorage.setItem and removeItem. But this is hacky, and since I don’t really need SSR for authorized users, I decided to just take the short way for now.

The best way, IMHO, would be to extend the sink object, to add methods which can optionally enable Meteor.user and Meteor.userId to work with those authenticated methods naturally during server-render. The way to do that is to probably use a cookie and give the server the ability to use the cookie to enable authentication inside that context. This is basically the fast-render approach to the problem. There are of course all the cookie hijacking things to worry about if we do that, but it’s probably the cleanest and most compatible way to do it. This should be a first class (and well tested/hardened) feature in the server-render and/or accounts-base packages.

There are some benefits to doing it the way I did though. It certainly reduces some back pressure on the server which now doesn’t have to worry about varying SSR for each and every user. That means we can use a front-end cache to capture server rendered routes, and serve those from a static content cache on the front. (And if we set that up, we don’t have to worry about the trickery required for getting renderToNodeStream working.) For installed PWA users, we might also skip SSR as well. Then we don’t have to worry about stale data in the cached app entry page. For my uses, SSR is all about that first load, and serving to bots (for open graph and SEO, etc.). Authenticated users should already have the js bundles and at least some data in their offline caches, so I’m less concerned about speeding up their reload times.