visit
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 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!
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 the contents for ourĀ .gitignore
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 . 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.
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.
In it, another reference to a file which we have not yet created: client.js
.
Letās create app/client.js
and then I will break it down.
And lastly, before we can test anything out, we also need app/App.jsx
.
<Helmet>
<title>Home Page</title>
</Helmet>
<div>
Follow me at <a href="[//medium.com/@patrickleet](//medium.com/@patrickleet)">[@patrickleet](//twitter.com/patrickleet "Twitter profile for @patrickleet")</a>
</div>
Now, you should be able to run npm run dev
to start your development server with hot code reloading!
> [email protected] dev Users/me/dev/patrickleet/stream-all-the-things> parcel app/index.html
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!
Create a file styles.js
:
In app/App.jsx
import GlobalStyles
:
And then change App
to render the GlobalStyles
component.
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
:
<Helmet>
<title>Home Page</title>
</Helmet>
<div>
Follow me at <a href="[//medium.com/@patrickleet](//medium.com/@patrickleet)">[@patrickleet](//twitter.com/patrickleet "Twitter profile for @patrickleet")</a>
</div>
Then, modify App.jsx
to have the following content:
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
:
<Helmet>
<title>About Page</title>
</Helmet>
<div>
This is the about page
</div>
And a loading component at app/pages/Loading.jsx
:
And finally an Error Component at app/pages/Error.jsx
:
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});
<Switch>
<Route exact path="/" component={Home} />
**<Route exact path="/about" render={() => <About />} />**
<Redirect to="/" />
</Switch>
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.
Create app/components/Header.jsx
:
And we need to import it and place it into our App
.
Hereās the updated App.jsx
:
const App = () => (<React.Fragment><GlobalStyles /><Header />
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/about" render={() => <About />} />
<Redirect to="/" />
</Switch>
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:
const Home = () => (<Page>
<Helmet>
<title>Home Page</title>
</Helmet>
<div>
Follow me at <a href="[//medium.com/@patrickleet](//medium.com/@patrickleet)">[@patrickleet](//twitter.com/patrickleet "Twitter profile for @patrickleet")</a>
</div>
</Page>)
export default HomeAnd do the same for the About
page, as well as the Error
and Loading
pages.
There are obviously infinite possible ways to style this app, so Iāll leave making things prettier as an exercise.
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.
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
:
/dist/clients
directory. We arenāt currently building production assets, but when we do, we can put them there.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.
"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.
Create server/lib/ssr.js
:
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)
}
And withserver/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.
Over in app/client.js
make the following modifications:
rehydrateMarks
and importedComponents
import { rehydrateMarks } from 'react-imported-component';import importedComponents from './imported'; // eslint-disable-line
2. Replace ReactDOM.render(app, element)
with:
Now, when you run npm run dev:server
or npm run build && npm run start
you will be using server side rendering!
Part 2: A Better Way to Develop Node.js with Docker_And Keep Your Hot Code Reloading_gzht888.com
Part 3: Enforcing Code Quality for Node.js_Using Linting, Formatting, and Unit Testing with Code Coverage to Enforce Quality Standards_gzht888.com
Part 4: The 100% Code Coverage Myth_Thereās a lot of advice around the internet right now saying that 100% coverage is not a worthwhile goal. Is it?_gzht888.com
Part 5: A Tale of Two (Docker Multi-Stage Build) Layers_Production Ready Dockerfiles for Node.js_gzht888.com