import() breaks SSR with react

Do you want to request a feature or report a bug?
A bug

What is the current behavior?
a combination of import() promises, webpack dependency resolution, and SPA architectures breaks SSR for react in webpack 2.

If the current behavior is a bug, please provide the steps to reproduce.

this is the scenario.

I’m writing a server that serves 2 routes, /a, and /b, rendering components <A/> and <B/> from files ./a.js and ./b/js.

On the server, for both routes, I’d like to render the html beforehand.

On the front end, I’d like to load bundles such that the server rendered page has its javascript available to pick up from server rendered html, but dynamically load the other chunk when it navigates to the other route.

That’s all. My contention is that this is broken with import()/require.ensure() being a promise.

Let’s dive into some code. First, the html ‘templates’ that’s hosting these scripts, a.html and b.html.

<!DOCTYPE html>
<html>
<body>
  {{ html }}  
  <script src='bundle.js'></script>
  <script src='a.chunk.js'></script>
  <script>App.start()</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
  {{ html }}
  <script src='bundle.js'></script>
  <script src='b.chunk.js'></script>
  <script>App.start()</script>
</body>
</html>

Simple enough. Here, we’re assuming a template system that uses handlebar style curlies, but you could substitute your own.

Of note, we preload the chunks matching the route, assuring that they’re available ‘instantly’. This is key for react to ‘pick up where it left off’ when rendering over SSR output. We’ll talk about how those chunks are made in a bit.

Let’s look at the components

// a.js
export default class A extends React.Component {
  render() {
    return <div>
      we on page A! <br/>
      <Link href='/b'>go to page B</Link>
    </div>
  }
}

// b.js
export default class B extends React.Component {
  render() {
    return <div>
      we on page B! <br/>
      <Link href='/a'>go to page A</Link>
    </div>
  }
}

we’re assuming the use of react-router@4 here, but you could setup your own pushState based system.

Now let’s look at our root component. We’re using react-modules to simplify the boilerplate.

class App extends React.Component {  
  render() {
    return <div>
      <Match pattern='/a' chunkName='a' render={() => 
        <Modules load={require('./a')}>{
            A => A ? 
              <A.default/> : 
              <div> loading... </div>
          }</Modules>
      } />
      <Match pattern='/b' chunkName='b' render={() => 
        <Modules load={require('./b')}>{
            B => B ? 
              <B.default/> : 
              <div> loading... </div>
          }</Modules>
      } />
    </div>
  }
}

Alright, now let’s talk about the mechanics. Assume we make a request for /a.

  • On the server-side, we use a sync require to get and render <A/>.
  • On the client side, to be able for pick up where the server left off, we need to have the A available immediately. So we first load entry script, then a.chunk.js, and then start the app.
  • With webpack 1, require.ensure would then resolve immediately, and render the content as expected.
  • However in webpack 2, because of the “always async” nature of promises, the module will not be availble for one tick. This leads to a mismatch, and it’ll try to render ‘loading’ for one frame, breaking SSR.
  • Workaround become further complicated, because unlike other async functions, the import callsite is semantically significant to webpack, which means we can’t just “hide” the machinery in a separate file. You’ll have to manually maintain a mapping of url patterns to modules. Sorta ick.

Here’s some pseudocode for the workaround. https://gist.github.com/threepointone/91670a9b5cf7dab254cc38ee983b3308

This is the commit that changed how require.ensure works in the background, making the promise unavoidable https://t.co/QLglJVC2Li If this went back to the previous behavior with regular callbacks, this issue can be considered fixed 🙂

I’m working on a repo with working example of the above situation, made this issue to quickly share with you folks though.

What is the expected behavior?

require.ensure resolves synchronously if possible.

If this is a feature request, what is motivation or use case for changing the behavior?

Please mention other relevant information such as the browser version, Node.js version, webpack version and Operating System.

webpack 2

Author: Fantashit

2 thoughts on “import() breaks SSR with react

Comments are closed.