I recently had a pleasure of using Next.js, the server side React framework, for building a small product — a free global tech job board. You can see it live at https://spaceboard.tech/.
I generally approach Server Side Rendering (SSR) with caution, because of it’s performance and complexity implications. However, SSR made sense for this application and Next.js really excels at developer experience.
Here are 5 tips for building a Next.js app.
1. Using Dynamic Routes
Next makes it easy to get started by treating all modules in pages
directory as distinct routes. E.g. visiting /about
renders the React component in pages/about.js
.
In most applications you need some dynamic parameters to render your page. For example, in the case of Spaceboard, the list of jobs could get long and is therefore paginated. To specify which page should be rendered, we could use a query parameter ?page=5
. Query parameters get passed to component’s getInitialProps as query and can be used to fetch the appropriate data.
It is possible to use parametrised URLs in Next instead, e.g. /page/2
instead of /?page=2
. Here’s the first tip: you’ll have much easier time if you stick to query params only and resist the temptation to use parametrised URLs. That is because if you use parametrised URLs, Next can no longer generate correct links in your application for you.
If you’re already planning to use a custom server implementation for other reasons, such as caching, or if you, like me, think that parametrised URLs just look so much cooler — read on.
I won’t go into the detail about how to setup a server for custom routing since Next.js has documentation and examples for that. But I’ll show you how I approached generating links throughout the application.
Since the parametrised route patterns are defined in a custom server outside of Next’s boundaries, Next doesn’t know anything about how to generate such links. So if you use the Link component it might navigate users to the wrong URL. For example, if you want to create a link that extends existing query parameters to add a filter, you’d expect it to work like this:
/page/2 -> /page/2?location=london
But, the Link component would link you like this:
/page/2 -> /?page=2&location=london
To solve this you can create a function that takes the current route, and the next desired query and pathname and returns a { href, as }
tuple. Here href is an internal Next link represtantation { pathname, query }
and as is the link we want to use in the browser’s address bar. You would then use this link generating function like so:
import Link from 'next/router'
import withRouter from 'next/router'
import link from '../utils/link'
function Header({ router }) {
const postLink = link(router, { post: 1 }, '/')
return (
<Link {...postLink}>
<a>Post a job</a>
</Link>
)
}
export default withRouter(Header)
Here is a gist of the implementation of the link function that I used.
If you make use of the new React hooks feature, this gets even nicer since you can access router via context in your hook:
// a React hook for generating links without the need to
// explicitly pass in the current route information
export default function useLink (nextQuery, nextPathname) {
const route = useAtom(state => state.route)
return link(route, nextQuery, nextPathname)
}
// usage becomes simpler
import Link from 'next/router'
import useLink from '../utils/useLink'
export default function Header () {
const postLink = useLink({ post: 1 }, '/')
return (
<Link {...postLink}>
<a>Post a job</a>
</Link>
)
}
Finally, the reason I chose not to create a wrapper Link component and used a function that returns { href, as }
is because sometimes you need to navigate imperatively without using the Link component.
2. React Hooks
In the previous section I‘ve mentioned React hooks. Hooks is a new, experimental feature shipped in React v16.7.0-alpha. Be minfdul when using this feature as the API could change in the next stable version of React.
Having said that, hooks work great together with Next.js. I used them for:
- accessing the current route information instead of having to wrap components using withRouter
- reading and writing cookies and abstracting away the client and server differences in the cookie API
- accessing state from a central store and triggering actions
- generating links as we saw in the previous section
- loading components asynchronously to speed up initial bundle size
Here’s a taster for how using hooks looks like:
export default function MyMegaComponent() {
// get the route information from the store
// you can put route information into the store
// within your _app.js component's getInitialProps
const { pathname, query } = useAtom(state => state.route)
// it's important to consider SSR when using cookies
// the cookie should be read differently on client and server
// on server, I stored a subset of the req.cookie information
// in my central store and I could access it inside the useCookie
// hook. For useCookie implementation, see
// https://gist.github.com/KidkArolis/d6bebe52862c8410db4b309d9c72099a
const [theme, setTheme] = useCookie('sb_theme')
// here we're using tiny-atom hooks, for more info see
// https://github.com/KidkArolis/tiny-atom#react-example
const { deleteJob } = useActions()
// generate a link that will update the query param to
// include location=london
const filterLink = useLink({ location: 'london' })
}
To get more ideas for how hooks could be useful in your applications check out https://usehooks.com/.
3. Central Store
You can do a lot in Next.js by leveraging the getInitialProps API and local component state. However, I always find the central store approach leads to a cleaner and more robust application implementation.
For example, in Spaceboard, I had to reflect the logged in user state in multiple places:
display Login vs Logout link dependong on whether user is logged in
prepopulate the Post form with the job details of the last job user posted
allow user to Logout in the form, inline, without losing form state
display admin controls in case the user has admin role
My go to solution for situations like this is to centralise all application state into a redux like store. I never got fully behind the Redux API and thefore always use an alternative, but the principle is the same. application state in a central store
To use a central store in Next requires a bit of juggling. There’s a good example in Next examples collection, but in principle you need to
- Create the store on the server
- Preload it with all the data required to render the page in getInitialProps
- Then recreate the store on the client
- Initialise it with the data serialised by Next and passed to the pages/_app.js component via props
- Phew 😅
Here’s how a stripped down pages/_app.js looked like in my app:
import React from 'react'
import App, { Container } from 'next/app'
import Head from 'next/head'
import createAtom from 'tiny-atom'
import { Provider } from 'tiny-atom/react'
import actions from '../actions'
const isServer = typeof window === 'undefined'
const getAtom = (state = {}, router) => {
const global = isServer ? {} : window
if (!global.atom) {
// pass router to actions, so that the navigate action
// could use router to navigate imperatively, this helps
// us completely abstract next's router
global.atom = createAtom(state, actions({ router }))
}
return global.atom
}
export default class MyApp extends App {
static async getInitialProps ({ Component, router, ctx }) {
ctx.atom = getAtom()
if (Component.getInitialProps) {
// the page should trigger appropriate store actions
// to load the store with the right data
await Component.getInitialProps(ctx)
}
// put a safe subset of the cookie into the store
// so that useCookie react hook could use the cookie values
// to render consisntely on server and client
ctx.atom.dispatch('receiveServerCookie', ctx.req.cookie })
// after the page data has been full fetched and loaded into the store
// dispatch the navigated action to update the store with the latest route
// information, this let's us read current route info from the store
// and avoids the need to use withRouter HOC
ctx.atom.dispatch('navigated', { pathname: ctx.pathname, query: ctx.query })
// finally, for the server scenario, return the current fully initialised
// store state so that Next could serialize it for us to rehydrate on the client
return { atomState: ctx.atom.get() }
}
constructor (props) {
super()
// recreate the store and initialise with the atomState from getInitialProps
this.atom = getAtom(props.atomState, props.router)
}
render () {
const { Component } = this.props
return (
<Container>
<Head>
<title key='title'>Spaceboard</title>
<meta name='viewport' content='initial-scale=1.0, width=device-width' key='viewport' />
<meta name='description' content='Free, fresh, global tech job board.' key='description' />
</Head>
<Provider atom={this.atom}>
<Component />
</Provider>
</Container>
)
}
}
With this setup, we can now:
- access and render any bit of state in any of the app’s components with a single line of code:
useAtom(state => state.user.loggedIn)
- avoid having to use
withRouter
to read the current route information - employ
useLink
anduseCookie
hooks that access route or cookie in the state - trigger actions to transition our state, e.g.
deleteJob
,countClick
,storeFilter
,navigate
,logout
There is one caveat when using this approach that I avoided to mention so far. And that is data fetching race conditions. If user clicks several links in rapid succession without waiting for the page to load, it’s important to either cancel the previous requests or at least ignore them to avoid rendering the wrong content. Normally Next.js handles this for us by only using the results of the lastgetInitialProps call. But by opting into using a central store, this becomes our responsibility. In the interest of time I won’t go into detail about how to handle this, but here’s one possible solution.
4. Server Side API
In my experience, it’s common to develop an API together with developing a UI. This was also the case for Spaceboard. Next.js already has a server component, this is the part of Next that renders your React pages into html and serves them up to the browser. How could we extend this server to also contain API endpoints of our application? And should we even be doing that?
What I find to be most effective for getting the best development experience and simplifying the deployment is to split Next and API into separate entry points for development but combine them into a single process for production. This is fairly straightforward when using the Fastify framework, but also possible with other server frameworks.
We create 3 entry points to our application, 2 for development and 1 for production. Here’s ./bin/next entry point that starts the Next server:
This is ./bin/api that starts the API server:
Now, you can run ./bin/next in one tab and nodemon ./bin/api in another tab of your terminal to get both the Next server and API server started and watching for changes to the code.
In production, we mount both next and api subapplications in the same process in the production entry point ./bin/www :
You can deploy this application to a server, for example, using Dokku and use ./bin/www as the entry point and have both Next.js logic and your API logic run in the single process, which simplifies things like monitoring.
One exercise left for the reader is the implementation of the next.js plugin used in the above snippets, but as usual, Next.js has a good example. For proxying the api requests in development I’ve used the k-fastify-gateway plugin.
5. Caching
If you have pages that don’t change frequently and want to be able to absorb traffic spikes, you have an option to wrap Next.js SSR rendering in a little bit of caching. To be fair, this shouldn’t be the first thing you do, Next.js is performant enough out of the box for your typical application. But it’s good to know this option exists, because rendering an entire page on the server using React isn’t the lightest of operations. As usual, Next has an example for how to achieve this.
Thanks for reading! I hope this post taught you something new you could use in your future development or gave you inspiration to try new workflows and tools. And please share https://spaceboard.tech/ to anyone you know is looking for a job or looking for people, every little bit helps.