How to use @loadable/server for meteor react apps

Ah dang. Thought I had it for you! Sorry, that was the only idea I had :frowning:

I’d need to see a reproduction repo. If you are wired for SSR, what it does is it loads all the loadables used to render a route on the server, renders it, then sends the list of loaded Loadables to the client along with the rendered HTML. Client side, it loads all of the Loadables necessary for the route to render before hydration - otherwise, you’d get weird flashing of content -> loading -> content, which you wouldn’t want.

Is is possible you are loading all the modules on the routes you are testing? If that’s the case, you definitely wouldn’t see the benefits from code splitting.

SSR is a trade off. It slows down your server’s response time (and increases stress on the server), but decreases the time to content. It probably also increases your time to interactive, because it lengthens the time of each stage to loaded. SSR takes longer than serving static content, and hydration has to wait for additional modules. You’ll have to decide whether these trade offs are useful for your specific app.

I’m still struggling with it. In order to make sure the setup on my side is correct I’ve read the documentation and have a few questions:

Under the chapter Server-Side Rendering you have the code snippet

preloadLoadables().then(() => {
  onPageLoad(sink => {
    sink.renderIntoElementById('root', renderToString(<App />)
  })
});

I guess you mean preloadAllLoadables?

Later you have the code snippet

import { LoadableCaptureProvider, preloadAllLoadables } from 'meteor/npdev:react-loadable'

preloadAllLoadables().then(() => {
  onPageLoad(sink => {
    const loadableHandle = {};

    const html = ReactDOMServer.renderToString(
      <LoadableCaptureProvider handle={loadableHandle}>
        <App/>
      </LoadableCaptureProvider>
    );
    sink.renderIntoElementById('root', renderToString(app))

    console.log(modules);

    sink.appendToBody(loadableHandle.toScriptTag())
  })
});

Where is the reference to modules? Inspecting loadableHandle.toScriptTag() shows me that the correct loadables were collected.

My code on the client side looks like this:

Meteor.startup(async () => {
  preloadLoadables().then(() => {
    loadLocale(store.getState().intl.locale).then(localeData => {
      addLocaleData(localeData.default);
      hydrate(
        <HelmetProvider>
          <Provider store={store}>
            <ReduxIntlProvider>
              <Router history={history}>
                <Route
                  name="main"
                  path="/:lang([a-z]{2})?/"
                  component={AppContainer}
                />
              </Router>
            </ReduxIntlProvider>
          </Provider>
        </HelmetProvider>,
        document.getElementById('root'),
        () => require('../imports/services/client/configure_persistor'),
      );
    });
  });
});


Here screenshot from webpagetest to illustrate what I mean. On 3 the js is loaded and on 29 all the other js content is fetched. I would expect that on 29 only the js relevant for the requested page to be fetched.

It looks like you found some errors in the documentation. Happy to fix those - PRs welcome.

If you inspect your source code from SSR, you should see a tag with the captured preloadables listed. What does that list show?

A reproduction project in git would make it easier to help you out.

The list shows only the loadables which are required on the root, i.e. <script type="text/ejson" id="__preloadables__">["/imports/scenes/Start/index.jsx"]</script>

There’s an error here - you should be passing the result of the first renderToString to sink, instead of rendering a second time:

 sink.renderIntoElementById('root', renderToString(html))

I’ve also been looking for some dynamic way to load components in my app and I found to this. I do like the idea behind but have some question.

  1. There is no typescript. On a huge project, it’s difficult to keep track of each component’s props. So It would be niche if the end result was a component with props-typing. The same goes for òpts`.
  2. @loadable/component offers things like this: const Moment = loadable.lib(() => import('moment')) to be used like this:
<Moment fallback={date.toLocaleDateString()}>
    {({ default: moment }) => moment(date).fromNow()}
</Moment>

I guess this would also be possible using this module.
3. What about NamedExports? Things like import { A, B, C } from 'module'? From the doc, this would require 3 different import for each named-import.

export default WrappedComponent => props => {
    const EchartsLib = loadable.lib(() =>
        import(/* webpackPrefetch: true,  webpackChunkName: "echarts" */ 'echarts')
    )
    const EchartsForReactLib = loadable.lib(() =>
        import(/* webpackPrefetch: true, webpackChunkName: "echarts" */ 'echarts-for-react')
    )
    return (
        <EchartsLib fallback={null}>
            {({ default: echarts }) => (
                <EchartsForReactLib fallback={null}>
                    {({ default: echartsForReact }) => (
                        <WrappedComponent
                            {...props}
                            echartsForReact={echartsForReact}
                            echarts={echarts}
                        />
                    )}
                </EchartsForReactLib>
            )}
        </EchartsLib>
    )
}

Wondering who can use @Loadable/Component with meteor, or you can configure Webpack in meteor, it seems impossible :joy:

I am referring to the SSR part, there is no problem with not using SSR

import { Loadable } from "meteor/npdev:react-loadable";
export default WrappedComponent => props => {
    const EchartsLib = Loadable({
      loader: () => import('./echarts'),
      loading: Loading,
    })
    const EchartsLib = Loadable({
      loader: () => import('./echarts-for-react'),
      loading: Loading,
    })
    return (
        <EchartsLib fallback={null}>
            {({ default: echarts }) => (
                <EchartsForReactLib fallback={null}>
                    {({ default: echartsForReact }) => (
                        <WrappedComponent
                            {...props}
                            echartsForReact={echartsForReact}
                            echarts={echarts}
                        />
                    )}
                </EchartsForReactLib>
            )}
        </EchartsLib>
    )
}

Something like that should work.

1 Like

npdev:react-loadable

Loadable({
		loader: () => import('../company'),
		loading: Loading,
	})

Can it be used directly? Where did fallback come from

Npdev: React -loadable can load lib?

NPDEV: React -loadable Can I import dynamic variables, like this

const AsyncPage = loadable(props => import(`./${props.page}`))

It would be great if we could,Where can I have more documentation or instructions to read, thank you :partying_face:

No you can’t, but it doesn’t have anything to do with react-loadable. The compiler needs the import statements to be statically defined, so it can built a list of dependencies. That’s the whole reason all these loader scripts need to take a factory method, instead of just a pathname string. This is a limitation of the dynamic modules spec, and not even of Meteor or any other implementation.

React Loadable is for loading react Components. For loading libraries, you can just use dynamic imports directly. It returns a promise you can handle however you like. (For libraries which return a component, you can use React Loadable though.)

Something like this should help you. Keeping a ‘/pages’ kind of a prefix ensures you are in good books of webpack too.

if (false) { // eslint-disable-line no-constant-condition
// for the compiler
	import('/pages/A');
	import('/pages/B');
}

const pages = {
	'/A': null,
	'/B': null,
};

for (const key in pages) {
	if (Object.prototype.hasOwnProperty.call(pages, key)) {
		pages[key] = loadable({
			loader: () => import('/pages' + key),
			loading: Loader,
			meteor: () => [require.resolve('/pages' + key)],
		});
	}

That won’t work because the builder for dynamic import needs a statically linked source value to build it’s manifest. (I thought this was true of the spec, but that may have changed - it’s definitely still true for most bundlers, including Meteor.)

It would because the import has already been read and compiled once because of the imports above in the code. Frankly, this would not help with reducing the bundle size and is more of a syntactical work around to handle “dynamic” variable imports.

But, using this in separate modules, and loading those modules dynamically for top level routes, helps reducing the bundle size as well :). Not perfect, but manageable.

That’s an interesting idea to avoid an otherwise bulky boilerplate.

Oh! Missed that. Yes you are right, it’ll definitely work in that case.

You are sort of emulating nextjs’s getStaticPaths and route based splitting strategy here.

There are arguably easier splitting methodologies to use with Meteor - but this would work.