Let’s talk first about libraries for creating universal applications. On GitHub you can find a large number of projects implementing the idea of universal web applications. However, all these projects have common drawbacks:
- missing functionality such as server side rendering (SSR) or i.e. hot reloading
- they are projects designed for quick start, not a library
- small number of project contributors
- projects were not updated
The first successful solution for easy server-rendered JavaScript apps, without those drawbacks was the Next.js library, which, has 650 contributors and over 33K “stars” on GitHub (at the time of writing). To evaluate the benefits of this library, consider what kind of functionality you need to provide for the operation of a universal web application:
-
Asynchronous data loading. In order for the results of asynchronous requests to be received prior to server rendering, Next.js implements a special type of the page component, which has an asynchronous
static async getInitialProps ({req})
life cycle event. -
Server-side rendering (SSR).
-
Passing server component state to client.
-
Creating a component on the side of a web browser and linking it to an HTML document.
-
Routing on the server and on the client should also be universal. That is, the same definition of routing should work for both server and client code.
-
Code splitting. For each page, only the necessary JavaScript code should be loaded, not the entire application. When moving to the next page, the missing code should be reloaded - without reloading the same modules, and without unnecessary modules.
All these tasks are successfully solved by the Next.js library. This library is based on a very simple idea. It introduces a new type of component - “page”, in which there is an async static method async getInitialProps ({req})
. The “page” component is a normal React component. This type of component can be thought of as a new type in the series: “component”, “container”, “page”.
Sample Project
Let’s create a simple project with Next.js to demonstrate how it works. You will be surprised how little you need to do in order to start our actual application. Some complexity is hidden by the library and the rule convention over configuration is helping to keep the code on our side really slim.
Create a new directory for our sample project mkdir sample-app && cd sample-app
. Generate an empty npm project here with npm init -y
. Then install Next.js together with React dependencies: npm i next react react-dom --save
.
Now we just have to add a new npm run script to our package.json
:
"scripts": {
"dev": "next"
}
Let’s try: npm run dev
. Unfortunately you’ll see an error message in your terminal. But with next.js, error messages are usually pretty good and will tell you exactly what you have to do next:
Couldn't find a 'pages' directory. Please create one under the project root
So go ahead, create a pages
directory and rerun npm run dev
. It will tell you that it is now running on port 3000 - so open localhost:3000 in your browser. It’s rendering a 404 page however because we did not yet create any “page”.
Let’s create an index.js
file with the following content:
export default () => <div>Hello World</div>
If you refresh your browser you’ll see our new awesome “Hello World” page. This sounds like very little but what actually happened under the hood could take you quite some time if you really do it from scratch:
- we got a free routing layer that is based on the convention that every path maps to a file in the pages folder (
/
is index.js,/hello
ishello.js
) - our code is automatically transpiled - we can use
export
without any hassle - we automatically can use react and
jsx
without any webpack setup - if you change the text from
Hello World
toHello World!
it will automatically be refreshed in your browser
But let’s focus on the “application” we want to build.
Showing hotel data
We want to show data about a hotel. So let’s create the necessary files for that:
For simplicity’s sake we will just use a static data file for the hotel data - src/hotel.js:
module.exports = {
id: '1aa4c4ad-f9ea-3367-a163-8a3a6884d450',
name: 'Dana Beach Resort',
description: '...'
};
And we will use that file in our pages/index.js:
import hotel from '../src/hotel';
export default () => {
return (<div>
{hotel.name}
</div>)
}
Check in your browser: It now shows us our hotel’s name.
Using another route and a button
What if we don’t want to show the data initially - we only want to reveal it after a click? Well that’s easy and we could solve it by adding another page to show the hotel data and link it form the index.js page:
pages/hotel.js
import hotel from '../src/hotel';
export default () => {
return (<div>
{hotel.name}
</div>)
}
pages/index.js:
import hotel from '../src/hotel';
import Router from 'next/router'
export default () => {
return (<div>
<button onClick={() => Router.push('/hotel')}>Show Hotel</button>
</div>)
}
Refresh your browser and click the button to go to the /hotel page. Again this looks like very little - but there is a bunch of stuff we get for free:
- we see our routing in action now. And additionally to links we can also imperatively use Router.push
- if you open your webdeveloper toolbar’s network tab you’ll see that this uses client side navigation - there is no full reload of our application
- and again in the webdeveloper toolbar you’ll see that we get “bundle splitting” for free: Each page has its own JavaScript bundle - resulting in awesomely fast pages.
Sure it is possible to achieve this with webpack or other bundlers manually as well - but with nextjs it is zero effort.
NOT using an additional page
Now let’s suppose we have a new requirement: we don’t want to have a new url for displaying the hotel. Instead we want a button on the index page that reveals the content - without any url changes.
We can use react and setState
to achieve this:
import React from 'react';
import hotel from '../src/hotel';
export default class extends React.Component {
state = {
showHotel: false
}
showHotel() {
this.setState({ showHotel: true})
}
render() {
if (this.state.showHotel) {
return <div>{hotel.name}</div>
}
return (<div>
<button onClick={() => this.showHotel()}>Show Hotel</button>
</div>)
}
}
But this has one downside: regardless if the user really looked at the hotel data - he has to load it in his browser. And our PO doesn’t like that because it slows down the page for ALL users even when only 1% of them actually click on the “Show Hotel”-button. For our little example that’s not much of a difference but in real world applications it sometimes is a huge improvement to only load what’s really needed.
We can use dynamic imports to achieve that:
Dynamic Imports
Next comes with a default webpack configuration that will make dynamic imports work out of the box. Let’s rewrite our index.js to use a dynamic import.
import React from 'react';
export default class extends React.Component {
state = {
hotel: null
}
showHotel() {
import('../src/hotel').then((hotel) => {
this.setState({ hotel });
});
}
render() {
if (this.state.hotel) {
return <div>{this.state.hotel.name}</div>
}
return (<div>
<button onClick={() => this.showHotel()}>Show Hotel</button>
</div>)
}
}
Instead of a boolean state we will introduce a hotel
state. When the user clicks on the button we dynamically import src/hotel.js
- this will yield a promise and when that is fulfilled we can set the result in the state.
Again this looks like not that much effort - but just compare this to the effort we would have had without next: E.g. with webpack we would have had to configure babel, plugins and so on.
Now open our application in the browser. Open your webdeveloper toolbar’s network tab and then click the button - you’ll see that it dynamically loads the JavaScript.
Improving our Dynamic Import
We can and should improve how our dynamic import works. It is awesome on fast connections. But how does it behave on slow connections? The user will click the “Show Hotel” button and nothing visually will happen for him - he has to sit and wait until the download is complete. Give it a try: open your webdeveloper toolbar and under the “network” tab throttle your network speed, and then click the button: It really sucks because there is zero feedback until the dynamic import is done.
We can solve this by introducing some kind of loading state: When the user clicks on the button we immediatley do a setState({ loading: true });
and show a spinner. When the code is loaded we do a setState({ loading: false })
again to hide the spinner. But this is tedious - and next has something easier:
next/dynamic
.
First let’s change our src/hotel.js
file a bit to actually expose a react component instead of just the data:
const hotel = {
id: '1aa4c4ad-f9ea-3367-a163-8a3a6884d450',
name: 'Dana Beach Resort',
description: '...'
};
export default () => {
return <div>{hotel.name}</div>
}
And then we can use the awesome next/dynamic
:
import React from 'react';
import dynamic from 'next/dynamic';
const Hotel = dynamic(import('../src/hotel'));
export default class extends React.Component {
state = {
showHotel: false
}
render() {
if (this.state.showHotel) {
return <Hotel />
}
return (<div>
<button onClick={() => this.setState({ showHotel: true })}>Show Hotel</button>
</div>)
}
}
Do you see what changed? We’re wrapping our dynamic import with dynamic()
. This will create a react component that will dynamically import the needed code in the moment when we actually try to render the component.
Sounds a bit weird at first and the benefits are not immediately obvious. But dynamic
has a second options
param.
And just changig it from:
const Hotel = dynamic(import('../src/hotel'));
to
const Hotel = dynamic(import('../src/hotel'),
{
loading: () => <p>LOADING...</p>
});
Will give us a “spinner” or loading screen for free. (Actually in newer react versions there is something comparable with React.Suspense - but that one only works on the client while next’s also works with SSR).
Styling
So how does styling work with next.js? Next bundles styled-jsx
so you can style your application by embedding <style jsx>
tags:
const hotel = {
...
};
export default () => {
return (<div>
<style jsx>{`
div {
color: red;
}
`}</style>
{hotel.name}
</div>)
}
While this looks alomst like using a normal style tag it has some benefits: it will only apply the styles to exactly this file. So no other div
will get a red color.
And if you don’t like styled-jsx you can always switch to sass, inline-css, css-in-js with just a little configuration.
Wrapping up
Next.js is a powerful tool for building single page application. As it follows convention over configuration principle, it makes it look easy to do this. Projects build with Next.js mostly have the same or very similar structure. Obvious use case where you definitely have to consider using Next.js would be building prototypes. It will be also handy for teams that don’t want to maintain their own webpack config and setups, but still have advantages that framework provides. Next.js is by all means production ready and would fit for enterprise application and not only some small projects. It is built in a way that enables you to access lower level parts when needed, which makes it extremely flexible in what it does. Not only is it really easy to use - it also has an amazing documentation. So far, every question we had was answered in their FAQ or through examples provided in their repository.