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 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. slightly 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 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. Hyperapp 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 with . Then we will cd into that directory and create a folder called and a folder called . Lastly, we will use to create our file. stream-all-the-things mkdir app server touch .gitignore 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- @ react-imported-component npm i --save-dev parcel-bundler react-hot-loader async 0.2 .0 Alright, a bit to unpack there. Though not much you havenāt seen before. Thereās the base dependencies youāve probably used before⦠, , plus . Then we also have styled-components to take advantage of . Beyond the fact that styled-components is a CSS-in-JS library that supports streaming rendering, I already preferred ! Itās opinionated approach helps to enforce best practices as well as being friendly to CSS developers. react react-dom react-router its streaming rendering support styled-components is an async version of the popular library 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 of the page. react-helmet-async react-helmet title Also, we have which will do the bundling, to nip some problems with Windows in the bud, , for our developing our server, for developing our client, and for cleaning up. parcel-bundler cross-env nodemon react-hot-loader rimraf 2. Development mode withĀ parcel Seems how our goal is to develop, letās start with development mode. Add a script in your scripts section of . dev 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 file we referenced. app/index.html <!DOCTYPE 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> < > 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 and then I will break it down. app/client.js React ReactDOM App { HelmetProvider } ; element = .getElementById( ) app = ( <App /> ) ReactDOM.render(app, element) ( .hot) { .hot.accept(); } import from 'react' import from 'react-dom' import from './App' import from 'react-helmet-async' const document 'app' const < > HelmetProvider </ > HelmetProvider // Enable Hot Module Reloading if module module And lastly, before we can test anything out, we also need . app/App.jsx React Helmet App = ( <Helmet> <title>Home Page</title> </Helmet> <div> Follow me at <a href="https://medium.com/@patrickleet">@patrickleet</a> </div> import from 'react' import from 'react-helmet-async' const => () < > React.Fragment ) export default App </ > React.Fragment Now, you should be able to run to start your development server with hot code reloading! npm run dev ā npm run dev > stream-all-the-things@ dev Users/me/dev/patrickleet/stream-all-the-things > parcel app/index.html Server running at http: ⨠Built ms. 1.0 .0 //localhost:1234 in 192 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 { createGlobalStyle } GlobalStyles = createGlobalStyle import from 'styled-components' export const ` /* 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 import : app/App.jsx GlobalStyles { Global Styles } import from './styles' And then change to render the component. App GlobalStyles App = ( <GlobalStyles /> <a href="https://medium.com/@patrickleet">@patrickleet</a> ) const => () < > div Follow me at </ > 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 from React Router, and then simply wrap our app with it. BrowserRouter In app/client.js { BrowserRouter } app = ( <BrowserRouter> <GlobalStyles /> <App /> </BrowserRouter> ) import from 'react-router-dom' // ... const < > HelmetProvider </ > HelmetProvider Now in 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 currently. app/App.jsx App.jsx Create : app/pages/Home.jsx React Helmet Home = ( <Helmet> <title>Home Page</title> </Helmet> <div> Follow me at <a href="https://medium.com/@patrickleet">@patrickleet</a> </div> import from 'react' import from 'react-helmet-async' const => () < > React.Fragment ) export default Home </ > React.Fragment Then, modify to have the following content: App.jsx React { Switch, Route, Redirect } Home App = ( <GlobalStyles /> <Switch> <Route exact path="/" component={Home} /> <Redirect to="/" /> </Switch> </React.Fragment> ) export default App import from 'react' import from 'react-router-dom' import from './pages/Home' const => () < > React.Fragment 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 React Helmet About = ( <Helmet> <title>About Page</title> </Helmet> <div> This is the about page </div> import from 'react' import from 'react-helmet-async' const => () < > React.Fragment ) export default About </ > React.Fragment And a loading component at : app/pages/Loading.jsx React Loading = ( ) Loading import from 'react' const => () Loading... < > div </ > div export default And finally an Error Component at : app/pages/Error.jsx React = ( ) import from 'react' const Error => () Error! < > div </ > 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 React { Switch, Route, Redirect } ; importComponent ; Home LoadingComponent ErrorComponent About = importComponent( ( ), { LoadingComponent, ErrorComponent }); App = ( <GlobalStyles /> <Switch> <Route exact path="/" component={Home} /> <Route exact path="/about" render={() => <About />} /> <Redirect to="/" /> </Switch> </React.Fragment> ) export default App import from 'react' import from 'react-router-dom' import from 'react-imported-component' import from './pages/Home.jsx' import from './pages/Loading' import from './pages/Error' const => () import "./pages/About" const => () < > React.Fragment 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 React ; styled { NavLink } ; Header = styled.header Brand = styled.h1 Menu = styled.ul MenuLink = styled.li () => ( <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> ) import from 'react' import from 'styled-components' import from 'react-router-dom' const ` 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 ` font-size: var(--step-up-1); ` const ` display: flex; justify-content: flex-end; align-items: center; width: 50vw; ` const ` margin-left: 2em; text-decoration: none; ` export default < > Header </ > Header And we need to import it and place it into our App. Hereās the updated : App.jsx React { Switch, Route, Redirect } ; importComponent ; { GlobalStyles } Header Home LoadingComponent ErrorComponent About = importComponent( ( ), { LoadingComponent, ErrorComponent }); App = ( <GlobalStyles /> <Header /> <Switch> <Route exact path="/" component={Home} /> <Route exact path="/about" render={() => <About />} /> <Redirect to="/" /> </Switch> </React.Fragment> import from 'react' import from 'react-router-dom' import from 'react-imported-component' import from './styles' import from './components/Header' import from './pages/Home' import from './pages/Loading' import from './pages/Error' const => () import "./pages/About" const => () < > 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 page: Home React Helmet Page Home = ( <Helmet> <title>Home Page</title> </Helmet> <div> Follow me at <a href="https://medium.com/@patrickleet">@patrickleet</a> </div> ) Home import from 'react' import from 'react-helmet-async' import from '../components/Page.jsx' const => () < > Page </ > Page export default And do the same for the page, as well as the Error and Loading pages. About 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 component that our client entrypoint loads. App 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: path express log ssr app = express() app.use( , express.static(path.resolve(process.cwd(), , ))); app.get( , ssr) port = process.env.PORT || ; app.listen(port, () => { log.info( ); }); import from 'path' import from 'express' import from 'llog' import from './lib/ssr' const // Expose the public directory as /dist and point to the browser version '/dist/client' 'dist' 'client' // Anything unresolved is serving the application and let // react-router do the routing! '/*' // Check for PORT environment variable, otherwise fallback on Parcel default port const 1234 `Listening on port ...` ${port} Ok, a couple things to unpack here: 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. 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. 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 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. ssr 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 file for the imported components for now. Maybe in the next few months this will change. .babelrc { : { : { : [ , ] }, : { : [ [ ] ] } } } "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 React { renderToNodeStream } { HelmetProvider } { StaticRouter } { ServerStyleSheet } { printDrainHydrateMarks } ; log through App { getHTMLFragments } (req, res) => { context = {}; helmetContext = {}; app = ( <StaticRouter location={req.originalUrl} context={context} > <App /> </StaticRouter> ); { sheet = ServerStyleSheet() stream = sheet.interleaveWithNodeStream( renderToNodeStream(sheet.collectStyles(app)) ) (context.url) { res.redirect( , context.url); } { [ startingHTMLFragment, endingHTMLFragment ] = getHTMLFragments({ : printDrainHydrateMarks() }) res.status( ) res.write(startingHTMLFragment) stream .pipe( through( { .queue(data) }, { .queue(endingHTMLFragment) .queue( ) } ) ) .pipe(res) } } (e) { log.error(e) res.status( ) res.end() } }; import from 'react' import from 'react-dom/server' import from 'react-helmet-async' import from 'react-router-dom' import from 'styled-components' import from 'react-imported-component' import from 'llog' import from 'through' import from '../../app/App' import from './client' // import { getDataFromTree } from 'react-apollo'; export default const const const < = > HelmetProvider context {helmetContext} </ > HelmetProvider try // If you were using Apollo, you could fetch data with this // await getDataFromTree(app); const new const if 301 else const drainHydrateMarks 200 ( ) function write data this ( ) function end this this null catch 500 And with we need to read in our file and break it into the two chunks that make streaming easier up above. server/lib/client.js app/index.html fs ; path ; cheerio ; htmlPath = path.join(process.cwd(), , , ); rawHTML = fs.readFileSync(htmlPath).toString(); parseRawHTMLForData = { $template = cheerio.load(template); src = $template(selector).attr( ) { src } } clientData = parseRawHTMLForData(rawHTML) appString = splitter = [ startingRawHTMLFragment, endingRawHTMLFragment ] = rawHTML .replace(appString, ) .split(splitter) getHTMLFragments = { startingHTMLFragment = [startingHTMLFragment, endingRawHTMLFragment] } import from 'fs' import from 'path' import from 'cheerio' export const 'dist' 'client' 'index.html' export const export const ( ) => template, selector = "#js-entrypoint" const let 'src' return const const '<div id="app"\>' const '###SPLIT###' const ` ` ${appString} ${splitter} export const ( ) => { drainHydrateMarks } const ` ` ${startingRawHTMLFragment} ${drainHydrateMarks} return 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 make the following modifications: app/client.js 1. Import rehydrateMarks and importedComponents { rehydrateMarks } ; importedComponents ; import from 'react-imported-component' import from './imported' // eslint-disable-line 2. Replace with: ReactDOM.render(app, element) (process.env.NODE_ENV === ) { rehydrateMarks().then( { ReactDOM.hydrate(app, element); }); } { ReactDOM.render(app, element); } // In production, we want to hydrate instead of render // because of the server-rendering if 'production' // rehydrate the bundle marks => () else And done! Now, when you run or you will be using server side rendering! npm run dev:server npm run build && npm run start 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! Best, Patrick Lee Scott 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! Part 1: Move over Next.js and Webpack Part 2: A Better Way to Develop Node.js with Docker Part 3: Enforcing Code Quality for Node.js Part 4: The 100% Code Coverage Myth Part 5: A Tale of Two (Docker Multi-Stage Build) Layers Part 6: Bring in the bots, and let them maintain our code