Build a TODO PWA with NextJS, Redux, TypeScript, Docker and Now Cloud V2 (Serverless Docker)

With offline capabilities

stack

Introduction

Hello there and thanks for stopping by. My name is Johhan Santana and I’ve been working with javascript for around 6 years now. I have been developing react applications with NextJS for around 2 years and for me is by far the best way to create react applications.

Please let me know if there’s something missing or if you have any questions while following this.

Enjoy!

What we’re building

We’re going to build a TODO list with NextJS, Redux, Docker and Now Cloud V2 and convert it into a PWA along the way for offline use.

Here’s the demo:
https://todo-pwa.now.sh/

The code is hosted at github if you want to give it a look or if you’re stuck somewhere.

NOTE if you already know how to setup a NextJS application, skip the getting started part.

Getting started

First of, let’s create a folder with a name of todo-pwa

mkdir todo-pwa
cd todo-pwa

Next, we’ll initiate our package.json file

npm init

Feel free to press enter on everything.

Now that we have our package.json created, let’s start adding the NextJS libraries and dependencies:

yarn add next react react-dom

After it completes, let’s open up package.json and update the scripts object to this:

After saving the file, create a new folder named pages in the projects root directory and create an index.js file:

mkdir pages
touch index.js

Open up index.js in your code editor and add the following:

Then run yarn dev in your terminal and open http://localhost:3000 in your browser and you should see Hello World!

Adding TypeScript

Let’s start by adding the dependencies:

yarn add @babel/core @zeit/next-typescript babel-loader typescript
yarn add --dev @types/next @types/react nodemon ts-node

Then change our scripts to match the following:

Next, create a next.config.js file in your projects root directory and add the following:

Let’s also create a .babelrc file:

Add a nodemon.json file as well:

And finally, let’s create our server by adding a server folder and a index.ts file insde it:

mkdir server && touch server/index.ts

Inside the newly created index.ts add the following:

Now in your projects root folder, let’s add typescript’s configuration files. We’re going to create 2 files, one for client side and one for the server side.

Let’s start with client side. Create atsconfig.json file and add the following:

Now create a second file named tsconfig.server.json and add the following:

Finally, rename our original index.js file insde our pages folder to index.tsx and run:

yarn dev

You should be able to open http://localhost:3000 and have the same original page we had before.

NOTE at this point, it would be good to add a .gitignore file to make sure we don’t commit to our repository unnecessary code with the following:

node_modules
.next

Adding redux

Now that we have our typescript boilerplate setup, let’s add Redux.

Start by installing the dependencies:

yarn add redux-devtools-extension react-redux redux

After, create a folder named actions inside it, create a file named todos.ts and add the following:

Now create a folder in the root directory of your project and name it reducers , add a file named todos.ts and add the following:

Now create another file in the same directory named index.ts and add the following:

Let’s create the redux store now. Create a root file named store.ts and add the following:

Since NextJS uses SSR (Server Side Rendering) we will have to configure Redux so it doesn’t execute twice.

Create a lib folder in the root application folder and add a file named with-redux-store.tsx and add the following:

Now let’s wrap our application to use our newly added redux store.

Create a file inside ours pages folder named _app.tsx and add the following:

Now run the app using yarn dev and go to http://localhost:3000

Open up google’s dev tools and if you have the redux extension, you will see that we have ourself a global store/state:

Make changes to our store

Now that we have our actions and reducers setup, let’s change our pages/index.tsx file to this:

Couple of things we did, let’s explain them.

  1. We converted our stateless component into a stateful component
  2. Imported a few redux functions as well as our addTodo action
  3. Setup our initial state for our form input value as '' for empty
  4. Setup onChange and onSubmit functions to use our form value and log some of the stuff we got in our component.
  5. Render returns a simple form with a single input and button
  6. Connected the redux state to our components props to add the todos
  7. Connected the redux actions to our components props to add the addTodo action

Now run the application again and open up your dev tools, go to console and start typing in your form’s input then press enter or click on the button.

You should see the following:

As we can see, you have yourself the input value in the state of the component, and the todos and the addTodo action from redux in your component’s props, meaning we can now start using them.

Let’s make some small changes to our onSubmit function:

...
onSubmit = e => {
e.preventDefault();
this.props.addTodo(this.state.inputVal);
};
...

Now go back to your app and type anything in your input then press enter or click on the button. Go to your redux devtools and you should see in your state that you have now a newly added todo string:

Keep adding them and test to see if they are being added to the list:

You have now finished setting up everything to be able to add a list to your redux store todo list. Hurray! (go take a coffee break or something)

Showing the todo list on the DOM

Now that we have our todo list inside our redux store, let’s start showing it in the DOM.

Let’s create a new folder in the root of our project named components and add a new file named todos.tsx and add the following:

This stateless component will accept our array of todos as props and iterate over them.

Let’s also add an index.tsx file in the same folder with the following:

import Todos from "./todos";
export { Todos };

Now in our pages/index.tsx file lets import our newly component and pass down the list as props:

...
import { Todos } from "../components";
...
render() {
const { todos } = this.props;
return (
<div>
<form>
<input type="text" onChange={this.onChange} />
<button type="submit" onClick={this.onSubmit}>
Add
</button>
</form>
<Todos todos={todos} />
</div>
);
}

We have imported our Todos component and added it after the form. Now go to your http://localhost:3000 and you should be able to see the todo list after adding some todos.

Adding some style with style-jsx

Let’s quickly add some basic styling to our poor looking app.

We’ll be using NextJS built-in style-jsx but we need to tell TypeScript that the <style> tag can now receive some special options.

Let’s create a folder named typings in our project root folder and create a file named styled-jsx.d.ts with the following:

Now that we got TypeScript happy, let’s change our _app.tsx file to this:

Our pages/index.tsx to this:

And our components/todos.tsx to this:

We should have something like this now when you run the app:

Nothing too fancy but it looks a bit better lol (help)

Take your time to inspect the new code but to sum it up, we

  1. Added google fonts in our _app.tsx file and some meta tags for responsiveness.
  2. In our pages/index.tsx we cleaned our input when submitted and added some styles with <style jsx></style> tag.
  3. In our components/todos.tsx we put a button on the left of each <li> and put a message if there weren’t any todos yet with some styling as well.

Removing todos

Now that we got the styling setup, let’s work on removing the todos we don’t want in our list anymore.

First, lets add our actions in actions/todos.ts

We will pass the index of the todo we want to remove from the todo list to our new action on line 20

Now in our reducers/todos.ts

In line 9 we clone the current state of todos for us to modify without touching the original state (it won’t work if you directly modify the original state) and remove the todo via its index.

Let’s modify our components/todos.tsx to receive the new removeTodo action

On line 4 we added a new prop that can be passed to this component and on line 14 we use it in our button.

Now on our parent component that is connected to redux let’s pass this action to the todos component:

First import the new action:

import { addTodo, removeTodo } from "../actions/todos";

then add it to the props types

type Props = {
addTodo: (text: string) => void;
removeTodo: (index: number) => void;
todos: string[];
};

Now add a function

removeTodo = (index: number) => {
const { removeTodo } = this.props;
removeTodo(index);
};

add the new action prop to our render method

const { todos, removeTodo } = this.props;

then pass down the prop to our Todoscomponent

<Todos todos={todos} removeTodo={removeTodo} />

Don’t forget to add it to our mapDispatchToProps function so it connects the removeTodo action to our components props

const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
addTodo,
removeTodo
},
dispatch
);
};

You should now have your file like this:

Now if everything is correct, you should now be able to add and remove todos:

Converting our app to a PWA

Now let’s convert our application to be a Progressive Web App.

Let’s install some dependencies:

yarn add sw-precache-webpack-plugin

In our next.config.js lets add the following:

This will generate the service-worker.js file for us.

We need to specify a manifest.json file as well, so let’s create a folder named static in our root projects folder and add the file called manifest.json with the following content:

This will be your app icons you can change them to whatever you like as long as they meet the specifications.

Now we tell our app where to find the manifest file. Edit the _App.tsx and add the following inside the <Head> tags:

<link rel="manifest" href="/static/manifest.json" />

Now in the same file, let’s register the service worker by adding the following:

componentDidMount() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/service-worker.js")
.then(registration => {
console.log("service worker registration successful: ", registration);
})
.catch(err => {
console.warn("service worker registration failed", err.message);
});
}
}

Our _App.tsx file should look like this now:

We have to tell our server to serve the static service-worker.js file. Add the following to the server/index.ts file:

const { pathname } = parsedUrl;
if (pathname === "/service-worker.js") {
const filePath = join(__dirname, "..", pathname);
app.serveStatic(req, res, filePath);
} else {
handle(req, res, parsedUrl);
}

Our server/index.ts file should look like this:

We’ve finished setting up our app to be a PWA. To test this, build and start your app for production using the following commands:

yarn build
yarn start

Open up your browser and go to http://localhost:3000 and open up the developer tools, you should see that your service worker has been successfully registered.

You can also try to put your browser in offline mode and refresh your app, you’ll see that you’re still able to load even when you’re offline, that’s the beauty of a Progressive Web App.

Using Redux offline

Now that we can download and play with our app offline, we want to be able to store our Todos as well. We’ll do this by storing the redux store in our device and initializing the redux store with a precache data.

Let’s add the dependencies:

yarn add redux-persist

And lets make a few changes to our store.ts file.

Import the dependencies:

import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";

Create the configuration for redux-persist:

const persistConfig = {
key: "root",
storage
};
const persistedReducer = persistReducer(persistConfig, todoApp);

and create the store accordingly:

export function initializeStore() {
const store = createStore(persistedReducer, composeWithDevTools());
const persistor = persistStore(store);
return { store, persistor };
}

Note that we are now returning 2 values, meaning we have to go to our lib/with-redux-store.tsx file and make a few changes.

First of, remove any anything that accepts a parameter with initialState since redux-persist will handle this now and change the render to this:

render() {
return (
<App
{...this.props}
reduxStore={this.reduxStore.store}
persistor={this.reduxStore.persistor}
/>
);
}

The file should look like this now:

Now go to our pages/_app.tsx and import the HOC for redux-persist:

import { PersistGate } from "redux-persist/integration/react";

And wrap it around here:

<Provider store={reduxStore}>
<PersistGate loading={null} persistor={persistor}>
<Component {...pageProps} />
</PersistGate>
</Provider>

Also add the persistor to our props deconstructed const:

const { Component, pageProps, reduxStore, persistor } = this.props;

Your file should look like this now:

Now run your app yarn dev and add some todos, refresh your browser and you’ll see your todos are still there! Perfect.

Done?

We have now completed our Todos app to work with Redux, TypeScript and convert it into an PWA with offline capabilities.

What we need to do now is deploy it so we can use it on the go in our different devices that support PWAs.

Dockerize the app

Now that we have our app completed, we will make a docker image out of it so we can deploy it to Zeit.co’s now with their new Serverless Docker.

Create a Dockerfile and copy the content from .gitignore to .dockerignorefile:

touch Dockerfile
cp .gitignore .dockerignore

Paste the following to the Dockerfile :

This will create a docker image with node preinstalled, copy our app source, install dependencies and run it and expose the image port 3000 (where our app internally runs with Node).

Now let’s build our image by doing the following in our terminal:

docker build -t johhansantana/todo-pwa .

Change johhansantana with your username of docker or anything you want really.

After it has built, we need to run a container from this image by executing the following in our terminal:

docker run --rm -d -p 8080:3000 johhansantana/todo-pwa:latest

Now go to your browser and open up http://localhost:8080 and you will see your app running from a docker container. Hurray!

Deploying to Now Cloud V2 (Serverless Docker)

We have our docker image ready to be deployed to now. Let’s configure our now.json

Create a now.json file in our project root folder:

touch now.json

Add the following:

In here, we’re specifying the name and alias of the now instance as well as the version of Now that we want to use, in this case, cloud v2.

Save the file and run now in your terminal.

Let it finish building and open a new tab in your browser and paste from your clipboard the url that the now-cli copies for you.

You now have yourself a hosted Todo PWA. Congratulations! Go test the url in your android phone and see how it prompts you to download the web app to your home screen!

With this Now setup we have a lot of advantages. More on this in their official blog.

Bonus, passing with 100 PWA audit

100 score on audit test

Let’s try and make our PWA pass the Audits with a 100 score!

Let’s give our app an theme_color in the address bar by adding this to _app.tsx :

<meta name="theme-color" content="#2F3BA2" />

Make sure to match the color with your manifest.json config theme_color as well.

Also add a <noscript> tag to show something when the browser doesn’t support javascript:

      ...
</Provider>
<noscript>Enable javascript to run this web app.</noscript>
</Container>

Your _app.tsx should look like this now:

Add another size icon option in the manifest.json file:

{
"src": "https://placehold.it/512x512",
"sizes": "512x512",
"type": "image/png"
}

save everything and build for production. You should now have a 100 score passing audit PWA, great!

Bonus 2, Making it super pretty!

So now let’s try and make our app look modern!

Something like this:

All of the new assets can be found in the github repository.

First we need to edit our todos.tsx component:

<div className="card" key={i}>
<li>
<p className="todo-title">{todo}</p>
<span
onClick={() => {
if (window.confirm("Remove todo?")) props.removeTodo(i);
}}
className="action-btn"
>
Done
</span>
</li>
</div>

And remove the <hr />.

Now lets put some of the css:

<style jsx>{`
.action-btn {
font-size: 12px;
opacity: 0.5;
cursor: pointer;
}
.todo-title {
font-size: 27px;
margin: 0;
opacity: 0.8;
}
.card {
min-height: 50px;
border-radius: 10px;
padding: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
background: white;
margin-bottom: 15px;
}
.ul {
list-style: none;
padding-left: 0;
font-size: 23px;
max-height: 68vh;
overflow: scroll;
padding: 0 10px;
margin: 0 -10px;
margin-top: 25px;
}
@media screen and (orientation: landscape) {
.ul {
max-height: 50vh;
}
}
.button {
margin-right: 5px;
}
.flex-list {
display: flex;
justify-content: flex-start;
align-items: flex-start;
}
`}</style>

Your component should look like this:

Let’s change the typography to a more modern one. Go to your _app.tsx and edit the google’s font link to this:

href="https://fonts.googleapis.com/css?family=Montserrat:400,500,600,700"

add a favicon reference

<link rel="icon" type="image/png" href="/static/icons/favicon.png" />

and change the theme color to this:

<meta name="theme-color" content="#c900ff" />

Your _app.tsx should look like this:

Now lets go to the index.tsx and make the following changes.

Add the title of the app and change the form tag like this:

<h1 className="title">TODO-PWA</h1>
<form className="form" onSubmit={this.onSubmit}>
...

Add a placeholder for your input:

placeholder="Type and press enter"

Remove the button since we won’t need it anymore.

Add the new css:

<style jsx>{`
:global(body) {
margin: 0;
padding: 0;
background: linear-gradient(180deg, #c900ff, #6e00ff) no-repeat;
font-family: "Montserrat", sans-serif;
}
.title {
margin-top: 0;
}
.form {
display: flex;
}
input {
outline: none;
}
.container {
width: 100vw;
height: 100vh;
margin-top: 25px;
margin-bottom: 25px;
display: flex;
justify-content: center;
align-items: flex-start;
}
.content {
background: #f9f9f9;
padding: 15px;
width: 500px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
border-radius: 10px;
height: 90vh;
}
.input {
width: 100%;
font-size: 20px;
font-family: "Montserrat", sans-serif;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
border: none;
height: 45px;
padding: 5px 10px;
border-radius: 10px;
}
:global(.button) {
font-size: 23px;
margin-left: 5px;
background: none;
cursor: pointer;
}
@media (max-width: 600px) {
.content {
width: 88%;
}
}
`}</style>

Your index.tsx should now look like this:

Now let’s edit our manifest.json file to have the following:

And we’re good to go. Go test your new modern looking PWA!


In this blog we learned how to setup a NextJS app with Redux and TypeScript, as well as using redux-persist to cache our redux store and not losing the data upon refresh. We also learned how to convert our NextJS app into a PWA (Progressive Web Application) with offline capabilities and how to deploy a Serverless Docker app to now.sh.

Thanks for reading!