Using React and Next.JS to build a PWA – The Beginner’s Guide
If you want to get started building PWAs with React, one of the things that you’ll notice is that most of the frameworks and guides out there tend to add a ton of libraries that you don’t actually need, which can quickly lead you down the wrong path both in terms of technologies and code quality.
Our approach for building PWAs is based on simple tools and techniques optimized for small codebases and high performance, which translates to faster and more maintainable products. One of our favorite stacks is React with Styled JSX under Next.JS, which is a fantastic framework. It has a ridiculously simple API with a ton of documentation and examples. It performs extremely well and gets updated very frequently with new and exciting features like server-side rendering and dynamic imports. The best part is that to get started you only need one JS file for the main page. One. That’s it. Seriously.
To help you learn the essentials behind this (featuring React, Styled JSX, Server Side Rendering, and some Next.JS features), we’re going to be building this Hacker News Reader. It’s a simple Progressive Web App to show you how everything works together, while covering some of the best practices you can apply to keep your code clean and simple.
We’ll be using React, Styled JSX, Next.JS, and Node.JS to take you all the way from starting a brand new Node.JS project to making your product live on a server so you can share it with the world. Enough chit-chat, let’s get started.
Setting Up Your Environment (~10 minutes)
First things first, we need to create a project folder to keep everything nice and tidy. And then, inside that folder we need to create a package.json file to document everything that’s going on in our app. You can do this by typing:
npm init -y
The npm init –y command will instantly generate a package.json for the project. This contains useful information about the app as well as development scripts so other developers can easily pick up where we left off.
Now we need to add the three main dependencies for Next.JS: next, react, and react-dom. We’ll also need isomorphic-fetch, so we might as well just add it now. As with all things NPM-related, this will take a couple minutes.
npm add next react react-dom isomorphic-fetch
Just to keep things neat, there are three scripts that should be standard for all JS projects:
- npm run dev: Run the app in development mode (with live reload, debugging, and other features).
- npm run start: Run the app in production mode (in a live server).
- npm run build: (Optional) Build the app so it can run in production mode.
For Next.JS to work properly, we need to input the following in the “scripts” section of package.json:
/* (...) */
"scripts": {
"dev": "next",
"start": "next start",
"build": "next build"
}
/* (...) */
Building Our First Page with React and Next.JS (~15 minutes)
The basic router for Next.JS is extremely simple: all pages are JS files placed in the pages directory and are searched by filename. For example, if we go to example.com/demo, it will look for pages/demo.js. If we go to example.com/unicorn, it’ll look for pages/unicorn.js. If we go to example.com (the index file), it’ll look for pages/index.js, and so on.
All respectable sites start with a home page, so we should create pages/index.js. In Next.JS, all pages are React Components, which in their simplest form are JS classes that extend React.Component and implement a render() method. Let’s take a look at a simple page:
export default class extends React.Component {
render() {
return <h1> Hello !</h1>
}
}
Wait, what’s HTML doing inside my JS file? Well, it’s not really HTML. It’s called JSX and it’s syntactic sugar we use for writing React components, so we don’t have to write as much code. You can check out the JSX in depth page of the React docs if you want to learn more about this. And yes, emojis are fully supported.
Now it’s time to run our dev environment (with live reload, nice errors, and a bunch of developer goodness). Go to your terminal and type:
npm run dev
Just leave it running for the rest of this tutorial, and if it somehow crashes just run it again. Here’s our Hello World page! Neat, right?
Next.JS supports live reload out of the box, so feel free to change the copy and watch it update live inside your browser.
If you want to add more elements (like a <p> for instance), you might see an error saying that “adjacent elements must be wrapped in an enclosing tag”, which is because React components can only return a single element. There’s two ways to go about this, though. If we’re using React 16 we can use the return multiple elements syntax, or we can use the classic HTML technique of just wrapping everything in a div like this:
export default class extends React.Component {
render() {
return <div>
<h1>Hello 🌎!</h1>
<p>This is some text</p>
</div>
}
}
You should take your newfound knowledge of React and Next.JS for a spin! There’s a few things that can give you a better feel for how it works and what the errors look like:
- Check out the live reload functionality and change the copy.
- Try making making a typo in one of the components.
- Add a couple of paragraphs, an image, or embed a YouTube video.
- Add a new page. Let’s say unicorn.js showing a nice unicorn emoji.
Adding some CSS with Styled JSX (~15 minutes)
Now that we have our home page working, it’s time to make our app a bit prettier. Times New Roman is not strictly speaking avant-garde typography. But don’t go looking for that CSS file just yet. Next.JS uses something called Styled JSX, which lets you style your React components one by one so we can keep our code neat and organized.
It’s quite simple to add styles to our page on pages/index.js. Just add a
<style jsx>{ /* Your CSS goes here */ }</style>
block inside your component and the styles will be applied like magic. It looks like this:
export default class extends React.Component {
render() {
return <div>
<h1>Hello 🌎!</h1>
<p>This is some text</p>
<style jsx>{`
h1 {
font-family: system-ui;
font-weight: 300;
color: #333;
}
`}</style>
</div>
}
}
Try adding some styles for that paragraph as well. It works just like regular CSS, and live reload works just as fine as before.
However, if you try to style the body, you’ll notice that nothing happens, no matter how hard you try. This is because Styled JSX applies exclusively to the component it’s in, so we can only style the div, the h1, and the p from that component (and anything else you may have added). No component outside (or inside!) will get those CSS rules applied.
There’s a way to get around that limitation: we can add global styles, which basically allows us to bypass this limitation and let our CSS rules fly. However, don’t abuse global styles for now, no matter how tempting that may be. We’ll see how to properly apply global styles in the next section.
/* (...) */
<style jsx>{`
h1 {
font-weight: 300;
color: #333;
}
`}</style>
<style global jsx>{`
body {
background: #eee;
font-family: system-ui;
margin: 0;
}
`}</style>
/* (...) */
If you need to make some very specific tweaks without using global style, you can also use Styled JSX’s :global selector, which lets you skip these boundaries as well and write more complex CSS rules. It’s generally intended as a last resort though.
By now you’re probably wondering where’s the SASS or PostCSS support, since “using plain CSS is super retro, grandpa.”
Hear me out before you start googling how to add them back in.
The main reason why we resort to coding standards like BEM, SMACSS, OOCSS, and their accompanying tools like SASS or PostCSS is that CSS is a horrible language. It has terrible scoping and namespacing capabilities so, as web apps became bigger and more complex, we ended up resorting to very complex naming schemes and tooling just to get around those limitations.
Styled JSX solves these problems by ensuring our styles are only applied to a specific component and absolutely nothing else. This means that our CSS can be incredibly simple and we can even get away with big no-nos like using divs and spans for our CSS, since we are only styling a handful of things at a time and there’s no need for complex rules. And for things like colors, we can just import our variables from a JS file.
What’s more, with Flexbox and CSS Grids it’s possible to have components whose only task is to build layouts, leaving us with very simple components that work everywhere in our app. Once you get the hang of this, you won’t miss the #horrible .workarounds .for-working–with span.CSS.
You can try out some other things to test your knowledge of Styled JSX:
- Add some styles to everything in your pages.
- Try creating a React component and adding it into your page. Add some styles to that specific component to get a better feel for how things work.
- If you’re still wondering how to use CSS preprocessors, you can check out the PostCSS and SCSS examples, which are just a matter of adding a couple of npm packages.
Using getInitialProps to show Hacker News stories (~10 minutes)
The classic example project for a PWA is building a Hacker News Reader, so that’s what we’ll be building. We’ll be using the Unofficial Hacker News API, which only has three endpoints.
And we start by introducing a concept that’s exclusive to Next.JS’s pages: getInitialProps() . This is a function that allows us to fetch all the critical data we need before rendering the page, and it’s used in order to power server-side rendering, which helps make our app faster.
- If this is our first visit to the site, the page is rendered server side, so we get our content faster as plain HTML. This also helps with SEO and sharing on platforms that don’t support JS-heavy web apps, which is nice.
- As we navigate the site, the pages are rendered on the client. Since we already have almost everything we need for the app, it makes sense to query the API directly from our browser.
Let’s give it a shot by querying the top posts from our index page. We’ll need to import the isomorphic-fetch library, which will ensure we have fetch() (a browser-only function) available on the server as well. If you want to build server-side rendered apps, looking for “isomorphic whatever” is usually a good idea to keep everything working well.
import 'isomorphic-fetch' /* So fetch works in the server and the browser */
export default class extends React.Component {
static async getInitialProps() {
const req = await fetch(`https://api.hackerwebapp.com/news`)
const stories = await req.json()
return { stories }
}
render() { /* Rendering Code */ }
}
If you refresh the page, you may have noticed that nothing seems to change in the browser. This is fine. We haven’t really changed our rendering code, so we shouldn’t expect any visual changes. But behind the scenes, Next.JS is getting the latest posts from Hacker News and injecting it into our component’s props in this.props.stories so we can use them in our app.
Let’s show the latest stories then! We can change our rendering code to show the newest stories by using the map() function, which will go over every story in this.props.stories and show a link in its place:
/* (...) */
export default class extends React.Component {
static async getInitialProps() { /* Get the Latest Stories */ }
render() {
return <div>
<h1>Latest News</h1>
{ this.props.stories.map((story) => (
<h2><a href={ story.url }>{ story.title }</a></h2>
)) }
<style jsx>{` /* Your Page’s CSS */ `}</style>
<style global jsx>{` /* Your Global CSS */ `}</style>
</div>
}
}
It works! Now you can see the latest stories from Hacker News in a few dozen lines of code. You should take this opportunity as a chance to throw some CSS into the mix and make the whole thing look a bit nicer.
Now, if you’re testing on your phone (and so you should), you’ve probably noticed that the app isn’t responsive in a strict sense. This is mainly because we’re missing the viewport meta tags. We’re also working on a PWA, which requires half a dozen meta tags in the <head> to keep everything working nicely, but there’s no obvious way to add them in.
Don’t worry, we’ll deal with that next! Dot JS!
{misc-bullet1}
Creating a Layout for our pages (~15 minutes)
Let’s tackle a bigger problem than adding things in the Head. If every page is its own component, how do you make a global layout so you don’t repeat yourself over and over again? As with all things React, it’s components all the way down.
Let’s add a new component that will standardize our layout, navigation, and styles across all of our pages. Since we’re super inspired, we’ll call it Layout. We’ll create it in its very own file to keep things neat. Since it’s not a page, we can’t use the pagesdirectory, so we’ll create a components directory and place our layout in components/Layout.js with this code. There are quite a few moving parts here, which we’ll explain below:
import Head from 'next/head'
export default class extends React.Component {
render() {
return <div>
<Head>
<title>{ this.props.title }</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
{ this.props.children }
</div>
}
}
Every React component has a simple API, called props (short for properties). We use these props to provide the components with the things they need to work. If you read through the code, you may have noticed that it’s using this.props.title and this.props.children, which we’ll cover below:
- The title property simply specifies the HTML
- The children property is a reserved property by React, which means “whatever is placed inside this component,” and is used to make the code more modular.
This means that if we do this,
<Layout title="Latest News"><h1>Hello!</h1></Layout>
the title property is going to mean “Latest News” and the children property is going to consist of the <h1>Hello!</h1> element we placed inside. That way we can use the Layout component to wrap whatever components we are using on our page and it’ll keep them as they are. It will also add all of those fancy meta tags and styles we need for things to work well.
Let’s try our Layout component in our pages/index.js file. We only need to import it and use it to wrap the contents of the page. We can just replace the div we were using with our new Layout, since we were only using it to keep things tidy.
/* (...) */
import Layout from '../components/Layout'
export default class extends React.Component {
static async getInitialProps() { /* Get the Latest Stories */ }
render() {
return <Layout title="Latest News">
<h1>Latest News</h1>
{ this.props.stories.map((story) => (
<h2><a href={ story.url }>{ story.title }</a></h2>
)) }
<style jsx>{` /* Your Page’s CSS */ `}</style>
<style global jsx>{` /* Your Global CSS */ `}</style>
</Layout>
}
}
If you refresh the page on your phone, you’ll notice that it now has all of the proper meta tags and the title is set, so we can find our app in the sea of tabs that is our browser.
There’s a bit more work to be done to help steer our app into PWA-land: We need to add a whole bunch of meta tags to keep things working. Go to your Layout and add the following meta tags in the Head, which will allow you to easily install your web app to your home screen on Android and iOS:
/* (...) */
<Head>
<title>{ this.props.title }</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#ff6600" />
<link rel="apple-touch-icon" href="/static/icon.png" />
<meta name="apple-mobile-web-app-title" content="Hacker News" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
</Head>
/* (...) */
You may have noticed that we’re linking to an apple-touch-icon here, which means we have to find a way to serve static files with our new stack. You’ll be happy to know that serving static files is super easy!
Just put your static files inside the /static folder. That’s it. If you want to get an icon for your project, feel free to take it from this github repo:
Aerolab/nextjs-pwa
Now that you know how to create modular components, I have a couple of challenges for you so you can polish the product some more:
- You should get rid of the global styles from your index page and move them to the Layout. It makes a lot more sense to reset the styles there, and it helps keep your code cleaner.
- We’re listing the stories in a way that is a bit… primitive. What if we split the list of stories into their own component and add more data, like the number of comments and how long ago was the story posted? We can call that component StoryList.
- You should try styling that component by itself as well.
- You should really add some static navigation on your layout. (What about an orange bar that says Hacker News?). A footer with copyright info would be nice as well.
{misc-bullet1}
Adding Comment Pages (~20 minutes)
There’s one more feature we can add to make our Hacker News reader complete, and that is the ability to read the comments (which is probably the best part of Hacker News). To do this, we need to create a new page, which we’ll call pages/story.js.
To show the comments, this page needs to receive a Story ID, which we can use to fetch and display the comments. We’re going to be using a query string for the time being, so our typical comment page will look something like example.com/story?id=12345. We will then use that ID to fetch the comments inside getInitialProps() and then render them as usual.
Let’s take a look at the code behind pages/story.js, shall we?
import 'isomorphic-fetch'
import Layout from '../components/Layout'
export default class extends React.Component {
static async getInitialProps({ query }) {
const req = await fetch(`https://api.hackerwebapp.com/item/${query.id}`)
const story = await req.json()
return { story }
}
render() {
return <Layout title={ this.props.story.title }>
<h1>{ this.props.story.title }</h1>
{ this.props.story.comments.map((comment) => (
<div className="comment">
<div dangerouslySetInnerHTML={ { __html: comment.content } }></div>
<div>By { comment.user }</div>
</div>
)) }
<style jsx>{` /* Your Page’s CSS */ `}</style>
</Layout>
}
}
You may have noticed there are a couple of new concepts in this page. We’ll explain them to make things clearer.
- In getInitialProps() we’re now getting the info for a specific item, which comes with all the comments bundled in one nice little package. You’ll notice a new prop here, that’s also exclusive to Next.JS: this.props.url.
- getInitialProps() gets passed an object with multiple request properties, including our beloved query . This contains the query args, which we need to get the story id we passed from the link in the home page by pointing to /story?id=${story.id} .
- What is dangerouslySetInnerHTML? This allows us to inject HTML directly into our page, which can be a security issue. We can trust the HN API not to do anything nasty, so we’re including the full HTML for each comment in our page.
- What is className? Remember at the beginning of this article where I said that JSX looks like HTML but really isn’t? Since we’re working with JS and class is a reserved word in JS-land, some HTML properties have been renamed. For instance, HTML’s class is now called className in JSX. It’s nothing major, just keep it in mind.
- You may have noticed we’re reusing the Layout component we created a few minutes ago, and now this page has the appropriate title for each story. That’s nice
There’s one more thing we need to do to make the Story page work properly: Actually link the stories from the home page. Otherwise it’s going to be kind of hard to navigate to them. Let me introduce you to a new component from Next.JS: Link. No, not the guy from Zelda. An actual link.
Link is kind of a weird component because you use it to wrap an actual a tag. So if you have to add a link for Link to work, why do you need it then? Because it does a bit of magic behind the scenes to help optimize your app a bit further:
- If you are opening a link inside your current tab, Link will intercept the request and only fetch the resources needed for that specific page, without downloading a huge chunk of JS again. Since we’re requesting as little data as possible from the server, each page we open is going to load a lot faster.
- You can add the prefetch prop to load the link in the background. If you use <Link prefetch />, it’ll load whatever it’s pointing to, so that when you click the link, the page loads instantly.
- If you are opening the link in a new tab or window, it won’t do anything and just fetch a brand new page from the server.
So let’s use the Link component on our pages/index.js page to link to our stories. We’re going to be linking to the /story page, as well as passing the story id as a query argument, with ?id=${story.id} . We’ll need this to fetch the data we need to display the comments page.
/* (...) */
import Link from 'next/link'
export default class extends React.Component {
static async getInitialProps() { /* Get the Latest Stories */ }
render() {
return <Layout title="Latest News">
<h1>Latest News</h1>
{ this.props.stories.map((story) => (
<div>
<h2><a href={ story.url }>{ story.title }</a></h2>
<p><Link prefetch href={ `/story?id=${story.id}` }><a>
{ story.comments_count } comments
</a></Link></p>
</div>
)) }
<style jsx>{` /* Your Page’s CSS */ `}</style>
</Layout>
}
}
We’ve now added a link to our comments page (including prefetching!) so everything’s ready to go the second we click the link to the comments. While prefetching is a fantastic feature, don’t add it willy-nilly on a thousand links or it’ll hug your server to death.
Since this is the last major feature we’re going to build for this tutorial, we’ll give you a few more suggestions so you can test yourself a bit further:
- You may have noticed that we’re only showing the top level comments instead of showing the entire comment tree. Well, that’s the challenge! Here’s a hint: you need to create a new React Component for showing comments (let’s say… <Comment />?), which may or may not include more Comments inside. Our good old friend recursion strikes again
- The home page is only showing 30 links or so. What if we added a next page link?
- The comments page really needs some styling. You should do a bit more CSS, especially for the comments.
{misc-bullet1}
Shipping with Now (~5 minutes)
We should totally ship our product so that we can publicly brag about our creation, but setting up servers and deploying things is no fun at all. Don’t worry, there’s a great product called Now that lets you ship your app with no configuration at all in like 30 seconds. It’s nuts. It’s also, incidentally, built by the same team behind Next.JS.
First of all, install now globally on your system:
npm install now --global
Now go to your project’s root folder (where your package.json lives) and type:
now
That’s it! If this is your first time using Now, it’ll ask for your email to set up an account. It’s smooth sailing from there and a ridiculously easy way to publish your projects. If you want to learn more about this, you should read our article on quick deployments with Now and Heroku
A quick guide to even quicker deployments – Aerolab Stories – Medium
{misc-bullet1}
Learn More about PWAs
Congratulations on making it to the end! You’re now the proud creator of a Hacker News reader built with the latest and greatest tech. As with all products, there’s probably an infinite to-do list in your head with things to add to it, but I can give you a few suggestions so you can learn more about how things fit together in Next.JS.
- Next.JS has an amazing collection of examples in their repo, as well as extensive documentation. Check them out. There’s a good chance that whatever you want to add to your app is covered somewhere in the docs.
- Service Workers let your app work offline and are an essential part of a great PWA. There’s a great example with sw-precache. Doing this requires a few changes, like adding two files (next.config.js and server.js) as well as modifying our index.js page to register the Service Worker on componentDidMount. Check out package.json, as the start and dev scripts also change a bit.
- Add a Web App Manifest as well! Link it inside your Layout file and place it in the /static directory. (Things in that folder are served automatically.)
- Lighthouse is an automated checklist for modern web practices. By doing the last two things (Offline and Manifest) you can get 100/100 on the PWA section of
- Adding Infinite Scrolling on the homepage would be a great way to put your React skills to the test.
- What about adding custom URLs? Those URLs we made are not strictly speaking awesome. Here’s a hint: Next.JS with router.
- You should add a nice domain for your app. You can use now alias to give the app a nicer URL than the random string you got from your first deploy.
That’s it for now! If you got your app working send a tweet our way so we can feature it on our site. Also, if you have any questions or suggestions, leave a comment below We’ll be happy to read you.