What I’ve learnt developing a modern progressive web app

Sanchit Nevgi
8 min readDec 30, 2016

--

I read about a lot of people who take up side projects as a challenge. Most recently about Mark Zuckerberg’s Building Jarvis. I thought I’d take one up as an experiment. There has been a lot of buzz going around progressive web apps or PWA being the next big thing in web. So I took a look at it and YOU WON’T BELIEVE what happened next…well you can take a guess.

Shut up and show me the code!. Take a look at the demo here.

Web development is moving at a crazy speed, like a Lamborghini on steroids. Javascript fatigue is real people. So I thought, how difficult would it be to write a modern offline-first web-app. After a lot of deliberation and thought, I landed on a progressive todo-app (I bet you’re really surprised)

Full disclosure: I am not even close to an experienced developer. I’m one of those “Oh f**k — check StackOverflow” type. I had just enough experience in React to barely know what’s going on. So don’t go pulling your hair out if things are not up to your standards. In order to comprehend a basic understanding of Javascript-land is needed. If you don’t but feel rebellious, carry on.

With the enthusiasm of a 5 year old on sugar rush, I created the project with create-react-app I yarned the dependencies up. The cogs in my brain began churning at 10rpm (I’m pretty slow).

I already had a mental model of what I had to do

  • Create a mobile-friendly todo app
  • Cache todos locally
  • Make it a PWA by adding offline capabilities
  • Deploy application
  • Add authentication
  • Store todos in the cloud
  • Add analytics, push notifications
  • Write blog post, bask in glory
  • Thank the president for the medal of honour

I wanted to use what all the cool kids were using. A lot of thought went into this. I finally landed on React, Redux, Webpack 2, ES6+7 (with all the generator goodies). I did not use a UI framework and went the flex way.

I set a deadline to do it on a weekend (Boy, was I naive). The “making the todo app” part was simple enough, having done countless tutorials. Sprinkle some actions and a dash of reducers and there we had it. MAN I’M AWESOME. And here comes our first lesson.

Lesson #1: Never judge a project by how easy it is to start.

actions/index.jsexport const ADD_TODO = 'ADD_TODO'
export const MARK_TODO = 'MARK_TODO'
export const EDIT_TODO = 'EDIT_TODO'
export const addTodo = text => ({type: ADD_TODO, text})
export const markTodo = (id, completed) => ({type: MARK_TODO, id, completed})
export const editTodo = (id, text) => ({type: EDIT_TODO, id, text})
reducers/index.jsconst todos = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
const todo = makeTodo(action.text)
return [ ...state, todo ]
case MARK_TODO:
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: action.completed }
: todo
)
case EDIT_TODO:
return state.map(todo =>
todo.id === action.id ? { ...todo, text: action.text }
: todo
)
}
}

I wired up a basic UI, mimicking material design as much as I could. Shout out to Chrome developer tools, you da bomb. If you aren’t using it already, I highly recommend that you do.

On to the next one…

We have a todo app, but it isn’t very helpful. The todos vanish on page refresh. We would need some way to cache todos and retrieve them on page reload.

A little birdie told me that IndexedDB was the way to go. localforage is a handy wrapper around IndexedDB and has api’s similar to localStorage. Using it was a piece of cake.

util/storageUtil.jsconst configStorage = () => {
localforage.config({
...
})
}
configStorage()
const getTodos = () => localforage.getItem(TODOS)const saveTodos = todos => localforage.setItem(TODOS, todos)export { getTodos, saveTodos }

Uh ohhhhh, our first roadblock. The redux docs say that reducers should be pure functions, and database access is not pure as much as I try to convince myself. A little overflowing (Thats what I call going on StackOverflow) later and I was left with two options redux-thunk and redux-saga. Both are very strong libraries. The Saga pattern seemed to be all the craze, so I went with that. Awesome! I checked out their docs to get started and…

Uh ohhhhh, I had no idea what async/await or generators were. At first, the docs were like a Shakespearean play to me. I had no f**king clue what was going on.

After some blog posts¹ and head-banging, I landed on something that worked. If you’d ask me how, I couldn’t tell you.

middleware/saga.jsfunction* cacheTodos() {
try {
const todos = yield select(state => state.todos)
saveTodos(todos)
} catch(e) {
yield e
}
}
function* getCachedTodos() {
try {
const todos = yield getTodos()
yield put(receiveCachedTodos(todos))
} catch(e) {
yield e
}
}
function* todoStorageSaga() {
yield [
takeEvery(FETCH_CACHED_TODOS, getCachedTodos),
takeEvery([ADD_TODO, EDIT_TODO, CLEAR_TODOS, MARK_TODO], cacheTodos)
]
}

And wiring it up with the redux store

index.jsconst sagaMiddleware = createSagaMiddleware()const store = createStore(rootReducer,   applyMiddleware(sagaMiddleware))
sagaMiddleware.run(todoStorageSaga)

Those few lines took me hours.

Whew! Now comes the interesting part (If you’re a nerd like me). So why make an app PWA anyway. If you don’t know what a progressive web app is, here’s a run down from the good people at Ionic

Progressive — Work for every user, regardless of browser choice, because they are built with progressive enhancement as a core tenet.

Responsive — Fit any form factor, desktop, mobile, tablet

Connectivity independent — Enhanced with service workers to work offline or on low quality networks.

App-like — Use the app-shell model to provide app-style navigation and interactions.

Installable — Allow users to “keep” apps they find most useful on their home screen without the hassle of an app store.

To make an app “progressive”, you need at the very least:

  • A service worker — contains caching logic
  • A manifest.json — contains your apps name, icon.

I followed the tutorial from Google and contrary to what I assumed, It wasn’t all that difficult. The guys are Google are so nice that they even provided a sw-precache tool, which let’s you lie on your butt and drink coffee while the tool does everything for you.

manifest.json{
"short_name": "Remember",
"name": "The simple Todo app",
"description": "A Todo app using progressive technologies",
"theme_color": "#009688",
"lang": "en-US",
"dir": "ltr",
"icons": [
{
"src": "favicon.png",
"type": "image/png",
"sizes": "192x192"
}
],
"start_url": "index.html?launcher=true",
"display": "standalone",
"orientation": "portrait"
}
package.json"scripts": {
"build-offline": "react-scripts build && sw-precache --root='build/' static-file-globs='build/**/!(*map*)'"
}

THAT’S IT!

Lesson #2: Just because something looks difficult, doesn’t mean it is.

Moving on.

It’s a good practice to deploy as early as possible if you’d rather not spend hours trying to figure out why the f**k npm build just won’t work.

I have been a fan of Firebase for a while now for its ease of use, and when they introduced hosting, I’ve been dying to try it out. Looks like this is our chance.

$ yarn global add firebase-tools
$ firebase login
$ firebase init
$ firebase deploy

SERIOUSLY, THAT’S IT! Man, technology is awesome! We got ourselves an app hosted at remember-53b67.firebaseapp.com

We have a deployed offline todo app, Woohoo! But wait, there’s a problem………the app sucks! There’s no way to edit a todo, you have to tap a todo to complete it, there are no animations and it just looks dull. Time to roll up my sleeves and summon my inner designer.

I decided adding coloured background to each todo item. This is where my newfound knowledge of generators came in handy. I added color-string, a handy library to manipulate colour strings.

util/colorUtil.jsconst constrain = (start, value, end) => Math.max(start, Math.min(value, end))function *getColor() {
const start = 40, jump = 8
let i=0
while(true) {
yield colorString.to.hex([0, constrain(start, start + i++*jump, 180), 0])
}
}
containers/TodoContainer.jsconst colors = getColor()<TodoItem ... bgColor={colors.next().value} />

This may not be the best way to do it, but hey, my brain runs at 10rpm, what do I know.

TL;DR: Added HammerJS and react-motion

Ladies and gentlemen, please fasten your seat-belts as we prepare for take-off

We have built this as a native app, so we need to handle touch events. HammerJS is a popular library that abstracts this and provides api’s such as tap, pinch, swipe. react-hammerjs is a wrapper around it that lets you add it as a React component.

The first thing we need it a way to edit todos on double-tap and save it on blur or enter. Easy-peasy

TodoItem.jseditTodo = () => {
this.setState({editing: false})
this.props.onEdit(this.props.id, this.state.editText)
}
handleDoubleTap = () => {
this.setState({editing: true})
}
handleBlur = () => {
this.editTodo()
}
handleKeyDown = e => {
if(e.which === 13) {
this.editTodo()
}
}
...<Hammer onDoubleTap={this.handleDoubleTap}>
{/* Actual todo */}
</Hammer>
TodoContainer.js<TodoItem ... onEdit={editTodo}/> // redux-dispatch passed as propreducers/index.jscase EDIT_TODO:
return state.map(todo =>
todo.id === action.id ? { ...todo, text: action.text }
: todo
)
actions/index.jsexport const EDIT_TODO = 'EDIT_TODO'
export const editTodo = (id, text) => ({type: EDIT_TODO, id, text})

P.S: This may seem over-whelming at first to the un-initiated, but trust me it’s easy once you understand it.

Cheng Lou’s react-motion is amazing for adding animation, although it takes some time to wrap your head around it. I’d recommend going through their docs and issues (I basically picked up the code from one of the demos). The code has been truncated for brevity, please don’t sue me.

TodoItem.jsconstructor(props) {
this.state = {isPressed: false, lastX: null, deltaX: 0, opacity: 1}
}
handleTouchStart = e => {
this.setState({lastX: e.touches[0].pageX, isPressed: true})
}
handleTouchMove = e => {
if(this.state.isPressed)
e.preventDefault()
let deltaX = e.touches[0].pageX - this.state.lastX
this.setState({deltaX, opacity: (240-deltaX)/240})
if(deltaX > 240) {
this.props.markTodo(this.props.id, true)
}
}
handleTouchEnd = () => {
this.setState({isPressed: false, lastX: null, deltaX: 0, opacity: 1})
}
render() {
let style = isPressed ? {x: deltaX, opacity} : {x: spring(0), opacity: spring(1)}
...
<Motion style={style}>
{({x, opacity}) => {
return <div style={{
backgroundColor: bgColor,
opacity,
transform: `translate3d(${x}px, 0, 0)`,
WebkitTransform: `translate3d(${x}px, 0, 0)`
}} onTouchStart={this.handleTouchStart} onTouchMove={this.handleTouchMove} onTouchEnd={this.handleTouchEnd}>
{/* Todo Item */}
</div>
}}
</Motion>
}

So far, we have a deployed an offline todo app, with some animations. Not bad, but far from over. Shit is about to get real. All in all, I’m pretty happy with myself. I know, I know, there are some ninja coders out there who could this blind-folded, balanced on top a horse while juggling fireballs (I would love to see that). In the next part, I’ll add cloud storage and authentication.

And finally,

Lesson #3: Even the simplest of apps takes tremendous effort if you want to do it right.

¹References:

ES6 Generators: [1] [2]

Progressive Web Apps: [1] [2] [3]

--

--