Move over Next.js and Webpack 🤯

Written by patrickleet | Published 2018/04/13
Tech Story Tags: react | nodejs | nextjs | webpack | server-side-rendering | next.js | styled-components | programming

TLDR Parcel is a newer module bundler in Javascript Land. It’s ZERO-CONFIG.css files, images, and whatever else you want and it works exactly like you’d expect it to. It makes it really easy to make universal applications that use all of the latest and greatest in the React ecosystem — code-splitting, streaming rendering, and even differential bundling — making it easy to get the latest in performance optimization with very little effort! We’ll be using a similar alternative to Next.js.via the TL;DR App

Simple Streaming SSR React with Styled-Components and Parcel

Photo via my Adobe Stock Photo license.
One of the things I loved about Next.js when I first used it was that it made the massive boilerplate required by Webpack almost disappear. It also prescribed simple, logical conventions that if you followed, allowed you to be easily successful.
I found it to be a huge step up in simplicity compared to the previous complexity of creating Server Side Rendered (SSR) React applications.
However, early last year I became aware of a new tool that could solve the same issues for me while staying closer to the core React API.
One of my biggest gripes with Next.js is it’s custom routing — although simple to use — the alternative React Router is really great, and there are great animation libraries that go with it, and I like creating pretty lookin’ easy to use things!
So in early 2018 I ditched Next.js and Webpack for something a bit “closer to the metal” and started building React apps with Parcel.
In this article I want to show you how I’ve been building apps with Parcel to create streaming server side rendered react apps with styled-components.
If you’re wondering what I’m so excited or haven’t tried out Parcel yet — Parcel is a newer module bundler in Javascript Land.
“Great another tool I have to learn” You think.
Nah. Parcel doesn’t roll like that. It’s ZERO-CONFIG.
It just works.
You can import .css files, images, and whatever else you want and it works exactly like you’d expect it to.
This makes it really easy to make universal applications that use all of the latest and greatest in the React ecosystem — code-splitting, streaming rendering, and even differential bundling — making it easy to get the latest in performance optimizations with very little effort!
I would like to use the new React lazy and Suspense APIs to implement the code-splitting, however, it’s still not supported on the server side, so we’ll be using a similar alternative.
In some cases it still may be slightly more verbose than Next.js, but for my use cases, I prefer the additional customizability. I think you will be surprised to see how simple things have gotten if it’s been awhile since you’ve evaluated your tooling.
This is intended for you to be able to follow along and end up with a nice new boilerplate.
I always have a personal goal for keeping things as lightweight as possible. If this weren’t SSR, I’d recommend checking out Hyperapp instead of React at all. I built a really cool JS SDK for a Shopify plugin that gave machine learning recommendations using it over the summer.
So what are we waiting for? Let’s get started!

1. Setup

First, create a new project with the following directory structure — one file, two folders.
- app/
- server/
.gitignore
We will make a directory called
stream-all-the-things
with
mkdir
. Then we will cd into that directory and create a folder called
app
and a folder called
server
. Lastly, we will use
touch
to create our
.gitignore
file.
Here’s a quick little snippet to do it. Feel free to type each line or copy and
paste the whole thing into your terminal.
mkdir stream-all-the-things && cd stream-all-the-things
mkdir app
mkdir server
touch .gitignore
Here’s the contents for our .gitignore
node_modules
*.log
.cache
dist
Next, let’s install the dependencies we will need.
npm init

npm i --save react react-dom react-router styled-components react-helmet-async@0.2.0 react-imported-component

npm i --save-dev parcel-bundler react-hot-loader
Alright, a bit to unpack there. Though not much you haven’t seen before.
There’s the base dependencies you’ve probably used before…
react
,
react-dom
, plus
react-router
. Then we also have styled-components to take advantage of its streaming rendering support. Beyond the fact that styled-components is a CSS-in-JS library that supports streaming rendering, I already preferred
styled-components
! It’s opinionated approach helps to enforce best practices as well as being friendly to CSS developers.
react-helmet-async
is an async version of the popular library
react-helmet 
that works with streaming SSR. It allows you to change information of in the head of the HTML document as you navigate. For instance, to update the
title
of the page.
Also, we have
parcel-bundler
which will do the bundling,
cross-env 
to nip some problems with Windows in the bud,
nodemon
, for our developing our server,
react-hot-loader
for developing our client, and
rimraf
for cleaning up.

2. Development mode with parcel

Seems how our goal is to develop, let’s start with development mode.
Add a
dev
script in your scripts section of
package.json
.
"scripts": {
     "dev": "parcel app/index.html"
}
With Parcel, you simple give it the entrypoint to your application as the only argument to start developing.
Now let’s create that
app/index.html
file we referenced.
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
    <meta content="utf-8" http-equiv="encoding">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">      
  </head>
  
  <body>
    <div id="app"></div>
    <script id="js-entrypoint" src="./client.js"></script>
  </body>
</html>
In it, another reference to a file which we have not yet created:
client.js
.
This is the entrypoint to our client application. In other words, the starting point. This is where out initial tree will be rendered.
Let’s create
app/client.js
and then I will break it down.
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { HelmetProvider } from 'react-helmet-async';

const element = document.getElementById('app')

const app = (
  <HelmetProvider>
    <App />
  </HelmetProvider>
)

ReactDOM.render(app, element)

// Enable Hot Module Reloading
if (module.hot) {
  module.hot.accept();
}
And lastly, before we can test anything out, we also need
app/App.jsx
.
import React from 'react'
import Helmet from 'react-helmet-async'
const App = () => (
  <React.Fragment>
    <Helmet>
      <title>Home Page</title>
    </Helmet>
    <div>
      Follow me at <a href="https://medium.com/@patrickleet">@patrickleet</a>
    </div>
    
  </React.Fragment>
)
export default App
Now, you should be able to run
npm run dev
to start your development server with hot code reloading!
âžś  npm run dev
> stream-all-the-things@1.0.0 dev Users/me/dev/patrickleet/stream-all-the-things
> parcel app/index.html
Server running at http://localhost:1234
✨  Built in 192ms.
Let’s check it out!
Because you are not me, try updating the page to a link of your own, and notice that you do not have to reload to see your changes!

3. Add some style

I use a mix of global styles, and styled-components.
Let’s add in some base resets and styles, as well as define a couple of useful CSS variables that will mathematically help us on our upcoming design adventures.
Create a file
styles.js
:
import { createGlobalStyle } from 'styled-components'
export const GlobalStyles = createGlobalStyle`
/* Base 10 typography scale courtesty of @wesbos 1.6rem === 16px */
html {
  font-size: 10px;
}
body {
  font-size: 1.6rem;
}
/* Relative Type Scale */
/* https://blog.envylabs.com/responsive-typographic-scales-in-css-b9f60431d1c4 */
:root {
  --step-up-5: 2em;
  --step-up-4: 1.7511em;
  --step-up-3: 1.5157em;
  --step-up-2: 1.3195em;
  --step-up-1: 1.1487em;
  /* baseline: 1em */
  --step-down-1: 0.8706em;
  --step-down-2: 0.7579em;
  --step-down-3: 0.6599em;
  --step-down-4: 0.5745em;
  --step-down-5: 0.5em;
  /* Colors */
  --header: rgb(0,0,0);
}
/* https://css-tricks.com/snippets/css/system-font-stack/ */
/* Define the "system" font family */
/* Fastest loading font - the one native to their device */
@font-face {
  font-family: system;
  font-style: normal;
  font-weight: 300;
  src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"), local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Light"), local("Roboto-Light"), local("DroidSans"), local("Tahoma");
}
/* Modern CSS Reset */
/* https://alligator.io/css/minimal-css-reset/ */
body, h1, h2, h3, h4, h5, h6, p, ol, ul, input[type=text], input[type=email], button {
  margin: 0;
  padding: 0;
  font-weight: normal;
}
body, h1, h2, h3, h4, h5, h6, p, ol, ul, input[type=text], input[type=email], button {
  font-family: "system"
}
*, *:before, *:after {
  box-sizing: inherit;
}
ol, ul {
  list-style: none;
}
img {
  max-width: 100%;
  height: auto;
}
/* Links */
a {
  text-decoration: underline;
  color: inherit;
&.active {
    text-decoration: none;
  }
}
`
In
app/App.jsx
import
GlobalStyles
:
import { Global Styles } from './styles'
And then change
App
to render the
GlobalStyles
component.
const App = () => (
  <div>
    <GlobalStyles />
    Follow me at <a href="https://medium.com/@patrickleet">@patrickleet</a>
  </div>
)
Your app should look slightly less ugly.

4. Routing

The next thing we need is for pages to be easy.
Let’s add in React Router.
In your client we need to import the
BrowserRouter
from React Router, and then simply wrap our app with it.
In
app/client.js
import { BrowserRouter } from 'react-router-dom'
// ...
const app = (
  <HelmetProvider>
    <BrowserRouter>
      <GlobalStyles />
      <App />
    </BrowserRouter>
  </HelmetProvider>
)
Now in
app/App.jsx
we need to extract our current content into a new component and load in through the router instead. Let’s start with creating a new page, using pretty much the same content as we have in
 App.jsx
currently.
Create
app/pages/Home.jsx
:
import React from 'react'
import Helmet from 'react-helmet-async'
const Home = () => (
  <React.Fragment>
    <Helmet>
      <title>Home Page</title>
    </Helmet>
    <div>
      Follow me at <a href="https://medium.com/@patrickleet">@patrickleet</a>
    </div>
    
  </React.Fragment>
)
export default Home
Then, modify
App.jsx 
to have the following content:
import React from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'
import Home from './pages/Home'
const App = () => (
  <React.Fragment>
    <GlobalStyles />
    <Switch>
      <Route exact path="/" component={Home} />
      <Redirect to="/" />
    </Switch>
  </React.Fragment>
)
export default App
Now when we run our app, it should look the same as before, except this time it is rendering through our router based on the match of the route /.
Before we move on, let’s add a second route, but this time with “code splitting”.
Let’s create a second page,
app/pages/About.jsx
:
import React from 'react'
import Helmet from 'react-helmet-async'
const About = () => (
  <React.Fragment>
    <Helmet>
      <title>About Page</title>
    </Helmet>
    <div>
      This is the about page
    </div>
    
  </React.Fragment>
)
export default About
And a loading component at
app/pages/Loading.jsx
:
import React from 'react'
const Loading = () => (
  <div>
    Loading...
  </div>
)
export default Loading
And finally an Error Component at
app/pages/Error.jsx
:
import React from 'react'
const Error = () => (
  <div>
    Error!
  </div>
)
export default Error
To import it, I’d like to make use of the new React.lazy and Suspense APIs, unfortunately, while they will work on the client, once we get to Server Side Rendering we will find that ReactDomServer does not yet support Suspense.
Instead, we will rely on another library called react-imported-component which will work with client side and server side rendered apps.
Here’s our updated
app/App.jsx
:
import React from 'react'
import { Switch, Route, Redirect } from 'react-router-dom';
import importComponent from 'react-imported-component';
import Home from './pages/Home.jsx'
import LoadingComponent from './pages/Loading'
import ErrorComponent from './pages/Error'
const About = importComponent(() => import("./pages/About"), {
  LoadingComponent,
  ErrorComponent
});
const App = () => (
  <React.Fragment>
    <GlobalStyles />
    <Switch>
      <Route exact path="/" component={Home} />
      <Route exact path="/about" render={() => <About />} />
      <Redirect to="/" />
    </Switch>
  </React.Fragment>
)
export default App
Now we should be able to navigate to /about to see our new page. If you look quickly, you will see Loading... appear before the page content.

5. Layout and Navigation

Right now we need to navigate via typing routes into the address bar, which is less than ideal. Before we move onto Server Side Rendering, let’s add a common layout to our pages and a Header with navigation to get around.
Let’s start with a Header so we can get clickin’.
Create
app/components/Header.jsx
:
import React from 'react';
import styled from 'styled-components'
import { NavLink } from 'react-router-dom';
const Header = styled.header`
  z-index: 100;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  max-width: 90vw;
  margin: 0 auto;
  padding: 1em 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
`
const Brand = styled.h1`
  font-size: var(--step-up-1);
`
const Menu = styled.ul`
  display: flex;
  justify-content: flex-end;
  align-items: center;
  width: 50vw;
`
const MenuLink = styled.li`
  margin-left: 2em;
  text-decoration: none;
`
export default () => (
  <Header>
    <Brand>Stream all the things!</Brand>
    <Menu>
      <MenuLink>
        <NavLink 
          to="/"
          exact activeClassName="active"
        >Home</NavLink>
      </MenuLink>
      <MenuLink>
        <NavLink 
          to="/about" 
          exact activeClassName="active"
        >About</NavLink>
      </MenuLink>
    </Menu>
  </Header>
)
And we need to import it and place it into our App.
Here’s the updated
App.jsx
:
import React from 'react'
import { Switch, Route, Redirect } from 'react-router-dom';
import importComponent from 'react-imported-component';
import { GlobalStyles } from './styles'
import Header from './components/Header'
import Home from './pages/Home'
import LoadingComponent from './pages/Loading'
import ErrorComponent from './pages/Error'
const About = importComponent(() => import("./pages/About"), {
  LoadingComponent,
  ErrorComponent
});
const App = () => (
  <React.Fragment>
    <GlobalStyles />
    <Header />
    <Switch>
      <Route exact path="/" component={Home} />
      <Route exact path="/about" render={() => <About />} />
      <Redirect to="/" />
    </Switch>
  </React.Fragment>
And let’s also create a Page component that each of our pages can use for a consistent Page style.
Create
app/components/Page.jsx
:
Then, in our four pages, import the new Page component, and replace the wrapping React.Fragment in each page with it.
Here is the
Home
page:
import React from 'react'
import Helmet from 'react-helmet-async'
import Page from '../components/Page.jsx'
const Home = () => (
  <Page>
    <Helmet>
      <title>Home Page</title>
    </Helmet>
    <div>
      Follow me at <a href="https://medium.com/@patrickleet">@patrickleet</a>
    </div>
    
  </Page>
)
export default Home
And do the same for the
About
page, as well as the Error and Loading pages.
Our app is starting to look a bit nicer!
There are obviously infinite possible ways to style this app, so I’ll leave making things prettier as an exercise.

6. Streaming Server Side Rendering

The next step for us to reach the our goal is adding in the streaming server side rendering. If you’ve been paying attention, you’ve noticed that so far we’ve created a static client side application.
Going from client side to isomorphic requires creating a new entrypoint on the server, which will then load the same
App
component that our client entrypoint loads.
We will also need several other new npm packages:
npm i --save llog pino express through cheerio 
npm i --save-dev concurrently rimraf nodemon @babel/polyfill cross-env
Let’s create server/index.js:
import path from 'path'
import express from 'express'
import log from 'llog'
import ssr from './lib/ssr'
const app = express()
// Expose the public directory as /dist and point to the browser version
app.use('/dist/client', express.static(path.resolve(process.cwd(), 'dist', 'client')));
// Anything unresolved is serving the application and let
// react-router do the routing!
app.get('/*', ssr)
// Check for PORT environment variable, otherwise fallback on Parcel default port
const port = process.env.PORT || 1234;
app.listen(port, () => {
  log.info(`Listening on port ${port}...`);
});
Ok, a couple things to unpack here:
  1. We are using express — it could easily be any other server. We’re really not doing much so it shouldn’t be too hard to convert to the server of your choice.
  2. We are setting up a static file server for the /dist/clients directory. We aren’t currently building production assets, but when we do, we can put them there.
  3. Every other route is going the ssr. Instead of bothering with routing on the server, we just do whatever React Router does.
Let’s create the
ssr
function. This will probably be more complicated than the rest of the tutorial, but it’s only something that needs to be done once, and then largely left alone.
Before we continue, let’s take a look at the scripts we need to create.
"scripts": {
  "dev": "npm run generate-imported-components && parcel app/index.html",
  "dev:server": "nodemon -e js,jsx,html --ignore dist --ignore app/imported.js --exec 'npm run build && npm run start'",
  "start": "node dist/server"
  "build": "rimraf dist && npm run generate-imported-components && npm run create-bundles",
  "create-bundles": "concurrently \"npm run create-bundle:client\" \"npm run create-bundle:server\"",
  "create-bundle:client": "cross-env BABEL_ENV=client parcel build app/index.html -d dist/client --public-url /dist/client",
  "create-bundle:server": "cross-env BABEL_ENV=server parcel build server/index.js -d dist/server --public-url /dist --target=node",
  "generate-imported-components": "imported-components app app/imported.js",
  "start": "node dist/server"
}
There are quite a few more now. I’ve highlighted the names to make it easier to read. At a high level, we added build scripts to generate a file containing info about imported components, as well as a build script which concurrently builds the client and server bundles using parcel.
We will also need a 
.babelrc
file for the imported components for now. Maybe in the next few months this will change.
{
  "env": {
    "server": {
      "plugins": ["react-imported-component/babel", "babel-plugin-dynamic-import-node"]
    },
    "client": {
      "plugins": [
        ["react-imported-component/babel"]
      ]
    }
  }
}
With that out of the way, we have two major pieces to solve.
Creating the SSR middlewareReusing the client HTML data for SSR and parsing the generated src name out of it
Create
server/lib/ssr.js
:
import React from 'react'
import { renderToNodeStream } from 'react-dom/server'
import { HelmetProvider } from 'react-helmet-async'
import { StaticRouter } from 'react-router-dom'
import { ServerStyleSheet } from 'styled-components'
import { printDrainHydrateMarks } from 'react-imported-component';
import log from 'llog'
import through from 'through'
import App from '../../app/App'
import { getHTMLFragments } from './client'
// import { getDataFromTree } from 'react-apollo';
export default (req, res) => {
  const context = {};
  const helmetContext = {};
const app = (
    <HelmetProvider context={helmetContext}>
      <StaticRouter
        location={req.originalUrl}
        context={context}
      >
        <App />
      </StaticRouter>
    </HelmetProvider>
  );
  try {
    // If you were using Apollo, you could fetch data with this
    // await getDataFromTree(app);
    const sheet = new ServerStyleSheet()
    const stream = sheet.interleaveWithNodeStream(
      renderToNodeStream(sheet.collectStyles(app))
    )
    if (context.url) {
      res.redirect(301, context.url);
    } else {
      const [
        startingHTMLFragment,
        endingHTMLFragment
      ] = getHTMLFragments({ drainHydrateMarks: printDrainHydrateMarks() })
      res.status(200)
      res.write(startingHTMLFragment)
      stream
        .pipe(
          through(
            function write(data) {
              this.queue(data)
            },
            function end() {
              this.queue(endingHTMLFragment)
              this.queue(null)
            }
          )
        )
        .pipe(res)
    }
  } catch (e) {
    log.error(e)
    res.status(500)
    res.end()
  }
};
And with
server/lib/client.js
we need to read in our
app/index.html
file and break it into the two chunks that make streaming easier up above.
import fs from 'fs';
import path from 'path';
import cheerio from 'cheerio';
export const htmlPath = path.join(process.cwd(), 'dist', 'client', 'index.html');
export const rawHTML = fs.readFileSync(htmlPath).toString();
export const parseRawHTMLForData = (template, selector = "#js-entrypoint") => {
  const $template = cheerio.load(template);
  let src = $template(selector).attr('src')
  return {
    src
  }
}
const clientData = parseRawHTMLForData(rawHTML)
const appString = '<div id="app"\>'
const splitter = '###SPLIT###'
const [ 
  startingRawHTMLFragment, 
  endingRawHTMLFragment 
] = rawHTML
      .replace(appString, `${appString}${splitter}`)
      .split(splitter)
export const getHTMLFragments = ({ drainHydrateMarks }) => {
  const startingHTMLFragment = `${startingRawHTMLFragment}${drainHydrateMarks}`
  return [startingHTMLFragment, endingRawHTMLFragment]
}
This will render our app via the server, however it won’t succeed in reconnecting to the client app without a few small changes to the client.
We are providing “rehydrate marks” via our SSR function, but not making use of them yet.
Over in
app/client.js
make the following modifications:
1. Import
rehydrateMarks
and
importedComponents
import { rehydrateMarks } from 'react-imported-component';
import importedComponents from './imported'; // eslint-disable-line
2. Replace
ReactDOM.render(app, element)
with:
// In production, we want to hydrate instead of render
// because of the server-rendering
if (process.env.NODE_ENV === 'production') {
  // rehydrate the bundle marks
  rehydrateMarks().then(() => {
    ReactDOM.hydrate(app, element);
  });
} else {
  ReactDOM.render(app, element);
}
And done!
Now, when you run
npm run dev:server
or
 npm run build && npm run start
you will be using server side rendering!

Conclusion

I’ll admit, there is still more boilerplate than Next.js, but hopefully it’s not overwhelmingly so, and what is there is transparent and understandable. And to be fair, Next.js is still doing a few more things for us, like prefetching components.
However, I still prefer this approach because there is no mystery in what is going on, webpack configs are completely gone, and it’s easy to make use of animation libraries for react router which I’ll leave as an exercise.
Hopefully you’ve found this useful!
If you did, the best way to help me is by giving me some claps and/or a share!
P.S. Here’s the full code on GitHub.
P. P. S. This article is part of a series. Check out the other parts below!

Written by patrickleet | HackerNoon's first contributing tech writer of the year.
Published by HackerNoon on 2018/04/13