Putting it all together with Universal imports

In this article we're going to look at what many have called the Holy Grail of the React/Webpack stack. Universal dynamic imports. We'll use Webpack's new import() function to split the code via the Routes we defined in the last article.

We're going to start where we left off in the last article.
If you need to catch up:

git clone https://github.com/lawwantsin/webpack-course.git
cd webpack-course
git checkout dynamic-imports
npm install

import()

The import syntax has been available since version 2.4. By using the ECMAScript 2018 standard, they're looking to stay future proof, while using require.ensure under the hood. Then there's the Universal component library by this guy. Who is really pushing the envelope as far as Universal dynamic imports for both client and server.

So to get started, let's look at plain ol' import.

Loading a Bundle on Click

In Gallery.js. Let's put a click handler on the heading. Your file should end up looking like this:

import React from "react"

const getBundle = () => {
  import("lodash").then(_ => {
    console.log("imported", _)
  })
}

export default () => (
  <div>
    <h1 onClick={getBundle}>Gallery</h1>
  </div>
)

If we run this we'll get an error. This is because babel doesn't understand import() as a function, only as a reserved word that, for static analysis reasons needs to be at the top of file.

In terminal:

npm i babel-plugin-syntax-dynamic-import

In .babelrc add the plugin to get the import syntax:

"plugins": ["syntax-dynamic-import"],

When npm run dev reruns, you'll have an extra bundle 0-bundle.js. When we click on the heading in our browser, we see the module is downloaded on click, which contains lodash. Pretty sweet!

No Names

No Names

But we probably want the chunk to have a better name. Just change one part of the config.

In webpack.dev-client.js and webpack.prod-client.js:

output: {
  filename: "[name]-bundle.js",
  chunkFilename: "[name].js",           // Add this line
  path: path.resolve(__dirname, "../dist"),
  publicPath: "/"
},

Now in Gallery.js add the "magic comment" to the import syntax. webpackChunkName names this bundle/chunk:

const getBundle = () => {
  import(/* webpackChunkName: 'lodash' */
  "lodash").then(_ => {
    console.log("imported", _)
  })
}

And the output is lodash.js. Pretty cool.

Chunk Names

React Universal Component

So React Universal Components makes loading Components in this way very easy. Let's look at that now.

In terminal:

npm install react-universal-component babel-plugin-universal-import

Let's add some stuff to our Routes.js file:

import React from "react"
import { Route, Link } from "react-router-dom"
import universal from "react-universal-component"
import { Switch } from "react-router"

const UniversalComponent = universal(props => import(`./${props.page}`))

export default ({}) => (
  <div>
    <div className="nav">
      <Link to="/">Gallery</Link>
      <Link to="/about">About</Link>
      <Link to="/article">Article</Link>
    </div>
    <Switch>
      <Route exact path="/">
        <UniversalComponent page="Gallery" />
      </Route>
      <Route path="/about">
        <UniversalComponent page="About" />
      </Route>
      <Route path="/article">
        <UniversalComponent page="Article" />
      </Route>
    </Switch>
  </div>
)

Universal component is using import to pull in a component in the relative path. Webpack looks at this directory and makes a separate bundle or "chunk" (same difference) as anything could be the value of props.page. We then pass it in as a prop inside the JSX of the component.

Notice the use of the Route component to wrap the UniversalComponents. What better interface for making websites could there possibly be? It's all building blocks. Last, we need ot wrap this in a Switch component or all components will be rendered.

In .babelrc add universal-import to the list of plugins:

"plugins": ["syntax-dynamic-import", "universal-import"],

This will let us use import() without the "magic comments" and still have proper chunk names output.

Speaking of output. When we run npm run dev we see the dev-server-bundle.js is making it's own chunks.

Dev Server Chunks

This is not desirable. Luckily, it's very fixable.

In webpack.dev-server.js and webpack.prod-server.js add this to your plugins:

new webpack.optimize.LimitChunkCountPlugin({
  maxChunks: 1
}),

Now it outputs only one. Okay, Cool. What about Prod? Works. It needs no other configuration, except what we've already done to the dev config.

Node Externals

Okay, but wait. It's not rendering on the Server Side anymore. Look it just says Loading... in the markup. What gives? I thought we were golden.

Our package we've been using webpack-node-externals is externalizing some stuff we'd rather it not. Specifically these universal packages. So we'll need to replace it.

touch config/node-externals.js

Let's setup a quick, custom mapping through the node_modules folder, picking out the packages we're going to want webpack to package into our dev-server-bundle.js.

const fs = require("fs")
const path = require("path")
const nodeModules = path.resolve(__dirname, "../node_modules")
const externals = fs
  .readdirSync(nodeModules)
  .filter(x => !/\.bin|react-universal-component|webpack-flush-chunks/.test(x))
  .reduce((externals, mod) => {
    externals[mod] = `commonjs ${mod}`
    return externals
  }, {})
externals["react-dom/server"] = "commonjs react-dom/server"

In webpack.dev-server.js:

const externals = require("./node-externals")

Then, inside the config, replace:

externals: nodeExternals(),

with

externals,

Now we can see SSR back in the source. Holy crap! You know what that means?

In Sum

We did it! Building a Boilerplate that is as good or better than any boilerplate out there. And since you built it from parts, you understand how everything fits together.

git checkout dynamic-imports-final

Up Next

We can also do dynamic CSS bundles. Let's build out our blog a bit in the next article and deliver dynamic CSS chunks for each of our JS chunks. We're really starting to see the finish line, so stay tuned.