Author
Lucas Aragno
Are you concerned about accessibility? Do you want your site to be blazingly fast and accessible, even from some random guy with a 2G connection on the North Pole? Are you afraid about being sued by Facebook? (JK probably not happening tho.)
Then you might wanna take a look to Preact, a lightweight alternative to React — and why not add learning about progressive web apps on the mix? This blog post attempts to show you how these two things works together by creating a little webapp to search TV shows.
Before we get started with Preact, lets start with the project configuration.
Setting up Babel and Webpack 2
A little note here. There are some Preact boilerplates / starter kits out there, but most of them are meant for complex apps and thus they install a lot of useful-yet-not-always-needed libraries and modules. I asked @_developit if he knew about a minimal set up for Preact apps and he came back to me with this helpful piece of art. You can skip this step and use his approach if you want to.
If you stick to my approach, first let’s create our project structure:
. ├── index.html ├── package.json ├── public ├── src
The directory src/ will contain the source code for our Preact app, public/ will hold all the builds for JS and CSS, index.html will contain the app barebones and package.json is all about our dependencies (and maybe some scripts).
Also, let’s get started by filling our index.html with some code like this:
<!DOCTYPE html> <html> <head> <title> TVApp </title> </head> <body> <div id="root"> </div> </body> </html>
Now let’s add some modern-js-fatigue-frontend-tooling. Read this carefully because your brains may fall out:
yarn add –dev webpack babel-loader webpack-dev-server babel-core babel-preset-es2015 babel-plugin-transform-react-jsx
Notice that I’m using Yarn, because I like live on the edge. You can use
npm install –save-dev
if you are not ready for this level of awesomeness yet.
If you are already a pro setting up your webpack and babel configs, feel free to skip to the next section. If you are not ready yet, after this you might end up using webpack a lot, or starting your very own js fatigue post. Either way you are gonna get something from it.
Let’s start creating the infamous webpack.config.js file:
var webpack = require('webpack') // require webpack of course var path = require('path') // path is nice to have module.exports = { entry: path.join(__dirname, 'src/index.js'), // our entry file output: { filename: 'bundle.js', // we are creating this file after bundling path: path.join(__dirname, 'public'), // this will be the folder for bundle.js publicPath: '/static/' // we are gonna serve the bundle from here using webpack-dev-server }, module: { rules: [ { test: /\.jsx?/i, loader: 'babel-loader', options: { presets: [ 'es2015' ], plugins: [ ['transform-react-jsx', { pragma: 'h' }] ] } } ] } }
I tried to keep things minimal, so no fancy configs, but this will do the work. The only “confusing” thing here is the path vs publicPath thing. For now just think this: for the final (production) bundle we are throwing that file on the public directory, but for webpack-dev-server (a little server that we are gonna use to work on dev) we are serving from /static/.
Now we need to tweak a little bit our package json in order to look like this:
{ "name": "moovieapp", "private": true, "dependencies": { }, "devDependencies": { "babel-core": "^6.23.1", "babel-loader": "^6.3.2", "babel-preset-es2015": "^6.22.0", "webpack": "^2.2.1", "webpack-dev-server": "^2.3.0", "babel-plugin-transform-react-jsx": "^6.23.0" } }
We have just added the “babel” key telling babel “hey we are using this preset”. You can move this thing from here into a .babelrc file if you want to.
And we are done. Yes, that was it, at least for now. A really painful experience right? (</sarcasm>)
So, let’s test our setup! we can create an index.js file on our src/ folder with the following content:
document.addEventListener('DOMContentLoaded', event => ( console.log('test') ))
I use DOMContentLoaded to wait for everything to load. You can use whatever you want (e.g. If you are into lispy things: (() => (console.log(‘test’)()))
Next, let’s tweak our index.html:
<body> <div id="root"> </div> <script src="/static/bundle.js"></script> </body>
Here we just added the <script> tag to load bundle.js from /static/.
Let’s take this for a run by doing node_modules/.bin/webpack-dev-server on our console:
Webpack should be running a server on your port 8080 in localhost and you should see the test message on your console! Pretty neat, huh?
That’s it for the configs, at least for now. Let’s step into the preact app now!
Getting started with Preact
We already have all the configs that we need for running Preact on our webpack config on the rules key:
... { test: /\.jsx?/i, loader: 'babel-loader', options: { presets: [ 'es2015' ], plugins: [ ['transform-react-jsx', { pragma: 'h' }] ] } }, ...
You could move all the presets/plugins stuff into a babel.rc file if you want to, but im trying to keep things (and files) minimal here. With this config we can use ES6 and Preact components.
If you are wondering about that ‘h’ after the keyword pragma, we will see that soon.
Let’s write our first/main component under src/compontents/App.js:
import { h, Component } from 'preact' export default class App extends Component { render () { return ( <div> Hi </div> ) } }
And now let’s get that rendered on our page by updating the index.js file so it looks like this:
import { h, render } from 'preact' import App from './components/App' document.addEventListener('DOMContentLoaded', event => ( render(<App />, document.getElementById('root')) ))
Now if you reload the page you should see our App component saying hi!
As mentioned earlier, you might have noticed that h here and there on the configs and the components. That’s a pretty common thing on Preact. You can read more about it here.
Working on our App
Now that everything is up and running, we can get down into some code. For this app I’ve just created a few components. Let’s go through each one:
This is how App.js looks now:
import { h, Component } from 'preact' import SearchBar from './SearchBar' import SearchesList from './SearchesList' export default class App extends Component { constructor () { super() this.state = { searches: [] } this.addSearch = this.addSearch.bind(this) } addSearch ({search, results}) { this.setState((prevState) => { const { searches } = prevState searches.push({search, results}) return {searches: searches} }) } render (props, state) { return ( <div> <SearchBar addSearch={this.addSearch} /> <SearchesList searches={state.searches} /> </div> ) } }
If you know React, this should be pretty straightcoforward: we have a constructor function and we keep a searches collection on the app state.
The addSearch method gets and object with a search and result keys as a parameter (thanks to destructuring) and we use the setState function to push the new search with its result to the new state.
Finally our render it’s just a <div> with 2 components SearchBar which will use addSearch as a callback later and SearchesList which will display the searches on our state.
Now, let’s create all the other components on the src folder.
The file SearchBar.js should look like this:
import { h, Component } from 'preact' import request from 'superagent' export default class SearchBar extends Component { constructor () { super() this.doSearch = this.doSearch.bind(this) } doSearch () { const { addSearch } = this.props const { text } = this.state const url = `http://api.tvmaze.com/search/shows?q=${text}` request .get(url) .end((err, res) => { if (!err) { addSearch({ search: text, results: res.body }) } else { console.error('Something is wrong with the API :(') } }) } render (props, {text}) { return ( <div> <input value={text} onInput={this.linkState('text')} /> <button onClick={this.doSearch}> Search </button> </div> ) } }
This component is an text input with a button to perform searches. It’s in charge of dispatching an API call and setting the results on a search within a searches collection on the App state. For that, we have the doSearch method that just gets the addSearch function from the props (the one that we sent from the App component) and the text input from the state. Then, it performs an Ajax call to tvmaze’s API. After that, if everything was ok, we call addSearch with our text as the search key and the results. You can see that I’m using superagent to handle Ajax calls, mostly because I’m super lazy right now. Feel free to use whatever you want, but if you want to use super agent as well, just run yarn add superagent on your terminal.
The render method has the input tag with the text from our state as a value and a really nice feature from Preact called linkState, which allows us to update the text key of our state on each change on our input element.
And we have also a button that triggers the doSearch function when it’s clicked. Notice that we had to bind that function to the component’s this on the constructor function so it could access the state and props of the component.
The file SearchesList.js is simpler:
import { h, Component } from 'preact' import SearchItem from './SearchItem' const SearchesList = ({ searches }) => ( <div> { searches.map((search) => <SearchItem search={search} />) } </div> ) export default SearchesList
Since it is just a presentational component, it doesn’t need state or lifecycle functions so we can just write it as a plain function that gets our searches from the state and maps them into a SearchItem
.
And SearchItem.js:
import { h, Component } from 'preact' const SearchItem = ({ search }) => ( <div> <h1> {search.search} </h1> <ul> { search.results.map(({show}) => ( <li> <h5> {show.name} </h5> <img src={show.image.medium} /> <div dangerouslySetInnerHTML={{__html: show.summary}} /> </li> )) } </ul> </div> ) export default SearchItem
This is also a presentational component so it’s also a function. Here we render a <div> with a title that should be the searched text (that’s what serach.search contains, sorry about the confusing naming) and we map the results inside a list showing the show name, image and summary (we use dangerouslySetInnerHTML because it comes down with it’s own html tags).
With all this new files and changes let’s go back to our localhost:8080 and we should see our app up and running:
Going offline
We have our app working nicely despite not having any styling, but what happens if we want to remember the description of a show we looked up before on the subway and we don’t have any connection?
That’s a shame we want our app to be available anywhere! That’s where service workers shine. Let’s get started making our app progressive.
First thing about progressive web apps is that they use a manifest.json file where we store some data about the app, much like when you work on chrome extensions. Ours should be something like:
{ "name": "TVApp", "short_name": "TVApp", "start_url": "/", "scope": "/", "display": "standalone", "background_color": "#2196F3", "theme_color": "#2196F3" }
And that manifest should be located on the root of our app. You could set more things in here like icons and such… feel free to tweak it around!
Next step, add the manifest on the <head> of our index.html like so:
<head> <title> TVApp </title> <link rel="manifest" href="/manifest.json"> </head>
Now we can add our service worker to cache our assets, let’s create a sw.js file also on the root of our app.
var CACHE_NAME = 'tv-app-v1' var urlsToCache = [ '/', '/index.html', '/static/bundle.js' ] self.addEventListener('install', function (event) { // Perform install steps event.waitUntil( caches.open(CACHE_NAME) .then(function (cache) { console.log('Opened cache') return cache.addAll(urlsToCache) }) ) }) self.addEventListener('fetch', function (event) { event.respondWith( caches.match(event.request) .then(function (response) { // Cache hit - return response if (response) { return response } return fetch(event.request) } ) ) })
There’s a lot going on here. Let’s split that into different parts:
First we declare the cache name, that can be whatever you want. After that, we declare an array of files that we want to store in our service worker cache. Since we are using the webpack dev server, we cache the /static/bundle.js, but if you are in production you probably want to add /public/bundle.js here.
Second, we open the cache and add all our url’s on the installation step of our service worker.
Third, every time our app does a fetch, we check if our cache contains the thing that the app is trying to fetch. If it does, we return the cached content. Otherwise, we retrieve the data from the network.
That’s it for now! Now we just need to install this service worker, in order to do that let’s add this on our index.html.
<!DOCTYPE html> <html> <head> <title> TVApp </title> <link rel="manifest" href="/manifest.json"> </head> <body> <div id="root"> </div> <script> if('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', { scope: '/' }) .then(function(registration) { console.log('Service Worker Registered'); }); navigator.serviceWorker.ready.then(function(registration) { console.log('Service Worker Ready'); }); } </script> <script src="/static/bundle.js"></script> </body> </html>
Here, we ask if the browser supports serviceWorker and if it does we grab our sw.js and we install it.
Let’s take it for a ride! If you are using Chrome you can reload the page, then go to your dev tools on Application -> Service Workers and there you’ll see your service worker registered.
Check that offline box and see how our app now even loads without connection.
But we still have a problem: we can see our app assets and if we perform a search without connection it’s gonna fail because it will try to use the network. Fixing that up is not really hard. We just need to make some tweaks on 2 files.
First on sw.js:
self.addEventListener('fetch', function (event) { var dataUrl = 'http://api.tvmaze.com/search/shows?q=' if (event.request.url.indexOf(dataUrl) > -1) { event.respondWith( caches.open(CACHE_NAME).then(function (cache) { return fetch(event.request).then(function (response) { cache.put(event.request.url, response.clone()) return response }) }) ) } else { event.respondWith( caches.match(event.request) .then(function (response) { if (response) { return response } return fetch(event.request) } ) ) } })
In our fetch event listener, we added an if, so if the request is for our API, we open the cache and then we store the result in there. Otherwise, we proceed as before returning assets from our cache or the network.
Now we have our requests on our cache, let’s read them from there. For that, let’s go to our SearchBar.js on the doSearch method:
doSearch () { const { addSearch } = this.props const { text } = this.state const url = `http://api.tvmaze.com/search/shows?q=${text}` if ('caches' in window) { caches.match(url).then((res, err) => { if(res) { res.json().then((json) => { addSearch({ search: text, results: json }) }) } }) } request .get(url) .end((err, res) => { if (!err) { addSearch({ search: text, results: res.body }) } else { console.error('Something is wrong with the API :(') } }) }
Here, we aso added another piece: checking if we have the request on our cache. If we do, we add the search from there, otherwise we go to the network again.
Let’s try our app again!
So, let’s say we have internet connection so we search for Batman shows:
But 5 hours later you are talking with your friend about the old Batman show while climbing a mountain and you don’t remember who was interpreting Batman on that show (how could you?). Now, you can check that on our app even without connection! (if you don’t have any mountain near you, you can try clicking the offline box on your chrome dev tools.)
That’s it! You’ve got your first Progressive Preact app up and running using Webpack 2! Probably there are bugs and things to improve. Feel free to hack around the repo and send PRs! Here you can find the source code.
Some final thoughts about Preact and Progressive web apps.
At first, I wasn’t really sure about Preact (or any other React alternative that matters), but after using it a bit I found it really useful. Specially when you want the React goodies, but you can’t / shouldn’t afford the bandwidth to send React on your build, like working on widgets or landing pages. Also, it plays really nice with progressive web apps and I found that the community is great!
And, for progressive web apps, I really thing that’s gonna play a major part on the future of web development. There are a lot of things to use on the ServiceWorker API, but it’s not standard yet. Keep an eye on this page to stay tuned.