Skip to content

Instantly share code, notes, and snippets.

@khaledosman
Last active March 10, 2020 18:24
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save khaledosman/de3535c8873831153efdf6c10a4b4080 to your computer and use it in GitHub Desktop.
Save khaledosman/de3535c8873831153efdf6c10a4b4080 to your computer and use it in GitHub Desktop.
create-react-app PWA example with epilog to update autogenerated service worker to skipInstall and show notifications using material-ui
const axios = require('axios')
const ms = require('ms')
function cachedGet (url, config, { cacheKey = url, isOnlineFirst = false, dontShowErrorMessage = false, showErrorMessage } = { cacheKey: url, isOnlineFirst: false, dontShowErrorMessage: false, showErrorMessage: () => {} }) {
// Get from cache and resolve if the access token is valid for best online performance as well as offline / lie-fi support, but make the call to the network anyway to update the cache for next visit. if there's nothing in the cache, fallback to the network
if (isOnlineFirst) {
// Online first approach
// Get from network then fallback to cache
return getFromNetworkAndSaveToCache(url, config, cacheKey)
.catch(error => {
if (!error.response) {
// Network error
if (!dontShowErrorMessage && error.message !== 'No cached response found' && error.message !== 'no token found' && error.message !== 'token expired') {
showErrorMessage({ message: `Couldn't complete request, please try again later` })
}
return getFromCache(url, config, cacheKey)
} else {
throw error
}
})
} else {
// Offline first approach
// Get from cache first, make the request anyway to update the cache then fallback to network
return Promise.race([
Promise.all([getFromCache(url, config, cacheKey), isTokenValid()]).then(([p1Res, p2Res]) => p1Res),
getFromNetworkAndSaveToCache(url, config, cacheKey)
])
.catch(error => {
console.warn('error', error)
if (!error.response) { // Network error or Results are not in cache
if (error.message !== 'No cached response found' && error.message !== 'no token found' && error.message !== 'token expired') {
// Network error
if (!dontShowErrorMessage) {
showErrorMessage({ message: `Couldn't complete request, please try again later` })
}
}
return getFromNetworkAndSaveToCache(url, config)
} else {
// let the consumer catch and handle the error
throw error
}
})
}
}
function isTokenValid () {
return new Promise((resolve, reject) => {
const authInfo = window.localStorage.getItem('authInfo')
const token = window.localStorage.getItem('token')
if (token && authInfo) {
const { access_token, createdAt, expiresIn } = JSON.parse(authInfo)
if (Date.now() >= new Date(createdAt).getTime() + ms(expiresIn)) {
reject(new Error('token expired'))
} else {
resolve(access_token)
}
} else {
reject(new Error('no token found'))
}
})
}
function getFromCache (url, config, cacheKey = url) {
return new Promise((resolve, reject) => {
const cachedResponse = window.localStorage.getItem(cacheKey)
if (cachedResponse) {
const response = JSON.parse(cachedResponse)
resolve(response)
} else {
reject(new Error('No cached response found'))
}
})
}
function getFromNetworkAndSaveToCache (url, config, cacheKey = url) {
return axios.get(url, { ...config, timeout: 40000 })
// .then(res => res.data)
.then(response => {
window.localStorage.setItem(cacheKey, JSON.stringify(response))
return response
})
}
module.exports.cachedGet = cachedGet
import { openSnackbar } from './components/notifier/Notifier'
import Button from '@material-ui/core/Button'
import * as serviceWorker from './serviceWorker'
serviceWorker.register({
onUpdate: registration => {
const onButtonClick = registration => e => {
if (registration.waiting) {
// When the user asks to refresh the UI, we'll need to reload the window
let isRefreshing
navigator.serviceWorker.addEventListener('controllerchange', function (event) {
// Ensure refresh is only called once.
// This works around a bug in "force update on reload".
if (isRefreshing) {
return
}
isRefreshing = true
console.log('Controller loaded')
window.location.reload()
})
// Send a message to the new serviceWorker to activate itself
registration.waiting.postMessage('skipWaiting')
}
}
openSnackbar({
message: 'A new version of this app is available.',
action: <Button color='secondary' size='small' onClick={onButtonClick(registration)}> Load new version </Button>
})
},
onSuccess: registration => {
openSnackbar({ message: 'This application works offline! Content has been cached for offline usage.' })
},
onOffline: () => {
openSnackbar({ message: 'No internet connection available.. The application is running in offline mode!' })
},
onOnline: () => {
closeSnackbar()
}
})
import React, { useState, useEffect, useCallback } from 'react'
import Snackbar from '@material-ui/core/Snackbar'
let openSnackbarFn
let closeSnackbarFn
export const Notifier = React.memo(function Notifier (props) {
const [open, setOpen] = useState(false)
const [message, setMessage] = useState('')
const [action, setAction] = useState('null')
const _closeSnackbar = () => {
setOpen(false)
}
const _openSnackbar = ({ message, action }) => {
setOpen(true)
setMessage(message)
setAction(action)
}
useEffect(() => {
openSnackbarFn = _openSnackbar
closeSnackbarFn = _closeSnackbar
}, [])
const handleSnackbarClose = () => {
setOpen(false)
setMessage('')
}
return (
<Snackbar
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
message={<span
id='snackbar-message-id'
dangerouslySetInnerHTML={{ __html: message }}
/>}
action={action}
autoHideDuration={20000}
onClose={useCallback(handleSnackbarClose)}
open={open}
SnackbarContentProps={{
'aria-describedby': 'snackbar-message-id'
}}
/>
)
})
export function openSnackbar ({ message, action }) {
openSnackbarFn({ message, action })
}
export function closeSnackbar () {
closeSnackbarFn()
}
"scripts": {
"build": "rm -rf build/ && react-scripts build && npm run-script sw-epilog",
"sw-epilog": "cat src/sw-epilog.js >> build/service-worker.js",
},
"dependencies": {
"@material-ui/core":""
}
// app.js
<Notifier />
// Add a listener to receive messages from clients
self.addEventListener('message', function(event) {
// Force SW upgrade (activation of new installed SW version)
if ( event.data === 'skipWaiting' ) {
self.skipWaiting();
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment