Meteor Packages vs NPM for modern bundles


#1

It looks like the maintainer of react-loadable won’t accept my PR for Meteor support (there’s actually an older PR for Meteor support too that has gone similarly ignored). So I’ve been looking at re-implementing the package, because, why not?

Really, I’ve been looking at forking react-loadable - there’s useful stuff in there I don’t want to have to figure out (like the babel plugin). But what I’ve been thinking about his how to get Meteor to compile the component for its modern/legacy dual bundle system (coming in 1.7, previously 1.6.2). Loadable as it is now, compiles with webpack, and pulls in some babel polyfill stuff, which bloats the final bundle.

What I’ve been thinking though, is how easy it is to simply expose the uncompiled javascript in a Meteor package to make sure it gets compiled with the rest of the app, vs. how to do that in an npm package. Is there actually a way to avoid pre-compiling an npm package for use with Meteor? There’s nothing in this which really requires a Meteor package, but it would make this aspect especially easy.


#2

I use react-loadable with Meteor. What issue are you having?


#3

React Loadable’s SSR doesn’t work with Meteor because the solution they implement assumes webpack - it requires both a babel plugin and a webpack plugin to get it working. The babel plugin needs a small change to work in Meteor, and the webpack plugin obviously does not work at all. Additionally, some of the assumptions made in Loadable’s architecture don’t apply to Meteor. For example, they assume a package -> bundle mapping, where Meteor’s dynamic-imports system doesn’t require that. They also assume a loading scenario we don’t have to worry about in Meteor’s server-render package.

You can read more about it all here. And check out the PR I made which is probably easier to understand than the micro-blog post, and includes instructions on how to set it all up here. I’m also working on starter repo which will implement this solution.


#4

Hmm. I preload all components on the server and allow the client to use dynamic-import as needed to retrieve components it hasn’t yet loaded. I’m assuming this is different than what you hope to accomplish: would you mind explaining how?


#5

If you hyrdrate your server side rendered HTML, do you get warnings?

On the client, you also need to pre-load all the components necessary to render your current route - but you wouldn’t want to preload all your Loadable components on the client, because you’d eliminate the value of code splitting.

So what I’m trying to do is get it to 1. render code on the server (this works out of the box) and 2. Hydrate that HTML quickly, without error (does not work out of the box - I mean it renders server side, then it runs client side, but it doesn’t hydrate - it re-runs). In order to do it properly you need to translate import statements (react-loadable’s babel plugin does that, but needs adjustment for Meteor), and also get a list of modules to load from the server (my patch enables a way to do that in Meteor, and in my strategy requires the paths to be root resolved) and then on the client side, you need to only load the modules used to render the current route (my example code in the PR shows how to do that in Meteor). React Loadable doesn’t do the additional steps out of the box, and even if you use webpack, you have to use the babel and webpack plugins to get it work correctly.


#6

Here’s another example. This compact function set (a simple async method proxy):

const callAsync = (methodName, ...args) => new Promise(
  (resolve, reject) => Meteor.call(methodName, ...args, (error, result) => {
    if (error) reject(error)
    resolve(result)
  })
)

const Methods = new Proxy({}, {
  get: (obj, key) => (...args) => callAsync(key, ...args)
})

When compiled for legacy browsers, becomes larger:

var callAsync = function callAsync(methodName) {
  for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
    args[_key - 1] = arguments[_key];
  }

  return new Promise(function (resolve, reject) {
    var _Meteor;

    return (_Meteor = Meteor).call.apply(_Meteor, [methodName].concat(args, [function (error, result) {
      if (error) reject(error);
      resolve(result);
    }]));
  });
};

var Methods = new Proxy({}, {
  get: function get(obj, key) {
    return function () {
      for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
        args[_key2] = arguments[_key2];
      }

      return callAsync.apply(undefined, [key].concat(args));
    };
  }
});

And that’s without packing in a Proxy or Promise polyfill, which would make a compiled module even larger. So I guess the question I’m asking is what’s the best way to deliver ES7+ code for consumption in Meteor apps? Is there a way to do that with NPM, or should I just use Atmosphere?


#7

I do not get warnings client-side. My react-router routes configuration has the following line:

if (Meteor.isServer) Loadable.preloadAll();


#8

I don’t know then why you don’t also see warnings client side - unless your “static routes” don’t actually load any Loadable modules? The problem I had was that the Loadables on the client (not the server side . that’s fine) would load the “Loading” module, which of course doesn’t match the HTML output from the server. I could make that invisible by simply manually preloading the modules I needed, but that wasn’t a scalable solution, and it would still kick out that warning.

I wonder, are you using React 16 and the hydrate method, or just rendering over the SSR output directly (on the client - again, I had no trouble server side)?


#9

Once I figure out how to properly mux helmet with react using renderToNodeStream I’ll post up my starter repo to show you what I’ve set up. I’m also using Loadable.preloadAll along with a workaround to prevent certain Loadables from getting caught up in that (non-static routes - or AuthRoutes/PrivateRoutes don’t need to be loaded on the server).


#10

#11

I came across that, and it looks pretty neat, but it does a few things I wasn’t comfortable with.

  1. It follows redirects on the server. This is okay for many apps (desirable even) - but for my app I’m not sure. I was concerned it would redirect a user who is logged in on the client (I don’t intend to track login on the server, or to use SSR for auth routes), who refreshes their page on an authorized route.
  2. It polyfills meteor subscriptions on the server, and then I think it uses the render cycle on the client to collect the list of subscriptions to wait for, which makes hydration impossible. It also doesn’t polyfill other missing client only APIs like Meteor.user() which makes it an inconsistent strategy. It does give me an idea though…
  3. It patches dynamic-imports. I just don’t like messing with internals that way. On the other hand, you are likely to catch more custom uses of dynamic-import for code splitting solutions than a solution which only targets Loadable, so that’s a bonus.
  4. It swaps HTML on client, instead of hydrating. In many cases this will not matter to the user, and they probably won’t even notice, but I wanted to do proper hydration.

It also (like my eventual solution until I implement a helmet cache) uses renderToString instead of renderToNodeStream.

The idea that #2 gave me was that we could use a similar polyfill on the server to capture subscriptions during SSR, and then pass that set of subscriptions to the client, similar to how I’m now doing loadable modules. This way we get precisely the subscriptions we need and have them subscribe before we hydrate!

BTW, here’s the starter I made with all this stuff built into it.


FastRender 3.0 is here! Now with SSR data hydration helpers