Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Equivalent to preloadAll #207

Closed
heygrady opened this issue Jan 18, 2019 · 5 comments
Closed

Equivalent to preloadAll #207

heygrady opened this issue Jan 18, 2019 · 5 comments

Comments

@heygrady
Copy link

heygrady commented Jan 18, 2019

I was upgrading our existing app from react-loadable to @loadable/components. In my previous server-side configuration I was relying on preloadAll from react-loadable to initialize all of the loadable components before starting my app.

Looking at the server-side example for loadable-compoents you are doing something similar in nodeExtractor.requireEntrypoint().

My problem: My app is set up slightly differently than the example and using requireEntrypoint would mean a significant refactor.

Why? Long ago we made the decision to build the entire express server using webpack/babel. In the example you build the react app twice with the exact same entrypoint and build the express server separately. That's an interesting approach but it's not the only approach.

Why preloadAll? You have recognized this issue on the client and provided a dedicated loadableReady function. On the server it would be nice to be able to resolve all loadables prior to running the app. Using requireEntrypoint only works if you're initializing a separately built app. Something like preloadAll allows you to initialize the app from within the bundle itself.

preloadAll is pretty clever. It works by keeping an internal reference for every loadable. It would be like adding to an array every time loadable is called. It then loops through those references recursively when preloadAll is called. Because it resolves each loadable recursively it's able to initialize the entire tree without the need to have a separate stats file for the server build.

In a server-side context the penalty is a brief pause while the server is initializing. Having something like preloadAll available would make it far easier to integrate loadable-components into a project that had been successfully running react-loadable 5.

Chicken and egg: There are some interesting issues with the way preloadAll works if you don't use node-externals in your server bundle. In the past I had designed apps to work more similarly to the server-side example and had issues with preloadAll. Something like requireEntrypoint would've made my life easier. However, I was able to solve the issue by re-exporting preloadAll from my entrypoint to ensure that it was referencing the version of react-loadable in the bundle. That way my server was able to import App and preloadAll to initialize the bundle before using it.

Example:

const { default: App, preloadAll } = require(path.resolve(
  __dirname,
  '../../public/dist/node/main.js',
))
const webStats = path.resolve(
  __dirname,
  '../../public/dist/web/loadable-stats.json',
)

app.get(
  '*',
  (req, res) => {
    const extractor = new ChunkExtractor({ statsFile: webStats })
    const jsx = extractor.collectChunks(<App />)

    const html = renderToString(jsx)

    res.set('content-type', 'text/html')
    res.send(`<!DOCTYPE html>
<html>
<head>
${extractor.getLinkTags()}
${extractor.getStyleTags()}
</head>
<body>
  <div id="main">${html}</div>
  ${extractor.getScriptTags()}
</body>
</html>`)
  },
)
preloadAll().then(() => {
  app.listen(9000, () => console.log('Server started http://localhost:9000'))
})
@gregberge
Copy link
Owner

Hello @heygrady, preloadAll() is impossible to do because of full dynamic imports:

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

Import function is not static, it depends of the context, for a different route you can have a different bundle loaded. This is why the function cannot be implemented in Loadable Components.

I hope it answers your question!

@heygrady
Copy link
Author

I believe that "impossible" is an overstatement.

React-loadable is able to do it and I am using it in production and it is working. I described part of their technique for accomplishing preloading. Even loadable-components offers a requireEntrypoint function that enables synchronous rendering.

Your point is that you may not quite know when the app will encounter an import, but in practice that is not as hopeless as it might seem. For instance, the example of a route is not an issue in a preloadAll scenario.

  1. The entrypoint will need to import the routes in order to render them
  2. Every route component is wrapped in a loadable, which is executed syncronously
  3. In a preloadAll scenario, those loadable calls that are encountered by the entry point are collected in an array somewhere
  4. When preloadAll is called we would resolve all of the known loadables. If a new loadable is encountered in the process then those are added to the array until all loadables are resolved.

One issue would be a loadable that is only encountered inside the render of a component. Again, in practice those loadables are not important in a server-side rendering context. Anything that happens asynchronously inside a component won't work on the server. The point of a preloadAll function is to recursively resolve every loadable encountered naturally just by instantiating the bundle.

I will be revisiting this at some point in the near future and I may be able to provide more details. I haven't looked into the code behind loadable-components enough to know how hard this would be to implement.

@gregberge
Copy link
Owner

React Loadable does not support aggressive dynamic import, this is why it can do it. We have to traverse the React tree to know what to do, and the only efficient way to do it is to call a render method of React. You can believe me, preloadAll strategy is not possible with dynamic import. I worked on Loadable Components since a long time, I have tested a lot of strategies.

I understand your point, but I don't want to create a preloadAll method that works only with "non-dynamic" import.

@bertho-zero
Copy link
Contributor

bertho-zero commented Feb 8, 2019

I also need a preloadAll, my server makes a page empty the first time and works for all subsequent times.

edit: I use esm for node.

@bertho-zero
Copy link
Contributor

bertho-zero commented Feb 8, 2019

I don't need a preloadAll if I replace https://github.com/smooth-code/loadable-components/blob/0d3e7dee3045737be1709ffa300aca083aef457c/packages/component/src/createLoadable.js#L166-L171
with

    Loadable.preload = function (props) {
      if (typeof window === 'undefined') {
        if (typeof ctor.requireSync === 'function') {
          return ctor.requireSync(props)
        }
        throw new Error('`preload` cannot be called server-side')
      }

      return ctor.requireAsync(props)
    };

The dynamic naming works with Node, with Webpack it's confusing at the time of the build but Node has everything to make a require that works at the time of the execution.
If there is a variable in the name, it is resolved in Node, not for webpack.

This code works perfectly with my modification if the part variable is not invalid:

const App = loadable( () => import( `./containers/App.${part}` ) );

And it will work for full-dynamic as well.

bertho-zero added a commit to bertho-zero/loadable-components that referenced this issue Feb 9, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants