This weekend, I finally got some time to work on my blog again after so long. I've been waiting Next.js to support custom server handler for some time and they finally did in v2.
My blog frontend is originally a simple Express setup with React as the component model. I use Gulp for build system for compiling server side code using Babel and bundle CSS with Autoprefixer. I don't write much logic in the client side aside from the iframe logic, some analytics & 3rd party integration script, so I just inlined them inside a script tag. It works pretty well, but it's not enough.
I have some plans to optimize my blog frontend more. But in doing so, I need to solve universal rendering problem which my current build system can't solve. This is why I'm so happy when Zeit open sourced their JS framework: Next.js.
Next is a framework for server-rendered React app. By declaring single component, Next will automatically reuse the same component in both server render and client render.
They use a simple convention to declare the component: put js file that export a React component inside pages/
directory. The filename will be used as path matching when server receives request. For example: pages/index.js
will be used to render http://localhost/
.
In the initial version, you have to create as many file as you need to render specific path (or use query string to pass a parameters). Starting v2 you can customize which file will render which url. This new feature allow me to use Next for the next iteration for my blog (pun not intended).
Migration Steps
Migration to other framework is usually hard. In my case, it was easier than I thought. Partly because Next is a React-based framework and I already use React for my server-rendered blog, and also because Next documentation is quite good and they have a ton of examples.
Gulp and Express
First thing I do is remove Gulp from build system. This is easily done by removing gulpfile.js
and remove all gulp modules from devDependencies
entirely. The next step is integrate Next into existing express app.
import express from 'express';import next from 'next';const app = next({ dev: __DEV__ });const server = express();app.prepare().then(() => { // all previous express path declaration goes after app.prepare() instead server.get('/', (req, res) => {});});
You might notice that I use __DEV__
variable. It's a global variable that is available inside build system that is basically show whether I run my app on dev or prod (more on that later).
Path Matching and Rendering Component
To tell Next which component is used to render which path or URL, we use express path matching. To send parameters to the rendered page, we use the 4th argument in app.render
method. We also use app.getRequestHandler
to handle any request other than to our page (example: JS and CSS asset).
server.get('/', (req, res) => { // pass through query string to our rendered page return app.render(req, res, '/home', req.query);});server.get('/:slug', (req, res) => { // merge query string with our slug to be used in page later const params = { ...req.query, slug: req.params.slug }; return app.render(req, res, '/post', params);});server.get('*', (req, res) => { const handle = app.getRequestHandler(); return handle(req, res);});
As you've seen above, app.render
accepts 4 arguments. The first 2 arguments is request and response object from http
or express
. The third parameter is used to get which filename will be used as the universal-rendered component. In my example above, accessing /
will render component from pages/home.js
, and /anything
will render component from pages/post.js
.
If you're wondering why we don't fetch post data before rendering and pass through the slug instead, it's because we want to be able to render the post page both server side and client side.
It's possible to put the post data in request / response object to render in server and use query string to fetch another post in client side but it's too complex.
Data Fetching
Next has a great concept for data fetching. You declare a static property in your React component called getInitialProps
. It is a function that returns a Promise that resolve the props you want to have in the component.
export default class PostPage extends React.Component { // instead of returning Promise, you can use async function // and returns the object directly static async getInitialProps(ctx) { // get slug from query object const { slug } = ctx.query; const response = await fetch('http://api.paperblog.com/post/${slug}'); const post = await response.json(); return { post }; } render() { // you'll have this.props.post here return <div />; }}
This method will executed in both server and client side (when navigating). This is why we put the slug in the query object.
When navigating to another post page, Next can use the same component with different query object and fetch it client-side. Essentially re-rendering the same PostPage
component with different props. DX & Perf win!
Custom Document
Because I don't want to make enormous changes when incorporating Next into my codebase, I keep using global classname and a global CSS file instead of styled-jsx
. This means I have to specify which CSS to load in the <head>
.
Fortunately, Next provides a way to override the html skeleton. We can do so by creating pages/_document.js
that export a class that extends Document
from next/document
.
import Document, { Head, Main, NextScript } from 'next/document';export default class PaperBlogDocument extends Document { render() { const { props } = this.props.__NEXT_DATA__; return ( <html> <Head> <title>{props.meta.title}</title> <link rel="stylesheet" type="text/css" href="/static/blog.css" /> </Head> <body> <Main /> <NextScript /> </body> </html> ); }}
Next will automatically resolve /static/
-prefixed url by returning the contents from static/
directory. I just need to move my blog.css
to static/
directory to achieve similar result with express.static
method that I used before.
We can also access the same props in client side by accessing this.props.__NEXT_DATA__.props
inside render method. I use this for getting the title
props value because it's resolved after fetching post data.
Next Custom Build Config
Next is really flexible that it provides 2 method to extend the config. First one is when you want to extend the babel config that Next use internally. You just need to create .babelrc
file in root directory and Next will recognize that and use that instead.
I use it to replace global variables (like __DEV__
) across my codebase using transform-define
plugin. Make sure you use next/babel
preset when extending, unless you know what you're doing.
The second method is to extend the webpack config itself. I haven't used it because the existing config is enough for me.
Server Build System
You don't actually need to set up a build system for server-side code if you're only using Next. In my case, I use async function in my server side code which is not supported yet in Node v6.9.4 (latest LTS). My first intuition is just use async-to-gen
module which is a small module to convert async function to generator which is available in Node 6.
But I also need to replace global variables like __DEV__
in server side code (that is not compiled by Next). Coincidentally I see a usage in build system section in async-to-gen
documentation. It's a Rollup plugin.
I never used Rollup before, so it's a chance to learn Rollup. Configuring rollup is easy and straightforward. Create a config file (similar to webpack) that specify an entry point and output file. Here is my rollup config
const replaceMap = { __DEV__: process.env.NODE_ENV !== 'production',};export default { entry: 'server/index.js', dest: 'server.build.js', plugins: [asyncToGen(), replace(replaceMap)], externals: ['next', 'express'], format: 'cjs',};
I use two plugin in my rollup config. rollup-plugin-async
and rollup-plugin-replace
. The first plugin is used to replace async function to generator.
The second plugin is used to replace variable values, similar to babel-plugin-transform-define
. It accepts a replaceMap
which is an object with key describing what variable you want to replace, and the value to replace.
I use the similar replaceMap
with the babel one with only minor difference: I need to wrap string inside JSON.stringify
call to use string value in Rollup replace plugin. Babel plugin one doesn't need this.
Finally the format property is used to specify which format the output will be. Because I used it for server side code, I use cjs
.
Caveat
Next uses webpack hot module replacement so it will connect to /__webpack_hmr
to update the module when developing locally. Because my blog urls use the root path and the /:slug
will match all paths except asset, I have to create another path to handle request to webpack dev server:
const handle = app.getRequestHandler();if (__DEV__) { server.get('/__webpack_hmr', (req, res) => { return handle(req, res); });}// place /:slug matcher after webpack hmr urlserver.get('/:slug', (req, res) => {});
Basically that's it.
I learn a lot when migrating my blog to use Next. I'm really happy that these tools exists so I can build something more easily. I don't have to use what I don't need. I can still use inline script (like I still do) or experimenting with some framework or build system. The choice is up to me.
Well, unless when I have to work for someone.