Ahmad Atallah
22 Apr 2020
ā¢
6 min read
Offline/Cache-first behavior is one of the main key features in any Progressive Web Apps, but according to
create-react-app docs:
"the offline/cache-first behavior is opt-in only. By default, the build process will generate a service worker file, but it will not be registered, so it will not take control of your production web app."
,
this will get you in a very disturbing state when your app is already built without registering service worker, and deployed to a group of
people who are using it regularly. Disturbing because users will not notice your updates if you decided to register the service worker unless
they explicitly hard reload the browser or even close all tabs. That's why I think that registering service worker should be a proactive decision
because it will affect the users who almost aren't familiar with offline-first apps. So, if you take a decision to go on with registering service worker,
it will be better to handle this from the first release of your app then users will get update message everytime your app updated.
In this article I will discuss how to register service worker correctly in your create-react-app
Progressive
Web Apps and notify users with any update goes live.
The solution requires a basic knowledge about service worker, react
, redux
and react-redux
the official react bindings for redux.
I will reference Create React App
documentation in this section just to get you familiar with
offline first apps.
ā¢ [Faster and reliable](https://create-react-app.dev/docs/making-a-progressive-web-app# why-opt-in):
Offline-first Progressive Web Apps are more faster and reliable than traditional apps because it actually caches all the static assets that
can be served by your app regardless of the network connectivty. Also, using manifest.json
file located in
the project public directory to add a mobile app version of the app without any need to install it from the store.
Defining your icon
, name
, and start_url
of your project are the main configurations for a mobile-app like view.
ā¢ [Require HTTPS](https://create-react-app.dev/docs/making-a-progressive-web-app# offline-first-considerations):
Service workers require HTTPS. That's why it doesn't apply to localhost
, not recommended in development environment, and it
can only be applied in production environment. So, better to test it by serving the build
directory using
serve
npm package.
npm install -g serve
ā¢ [No interception with cross-origin resources](https://create-react-app.dev/docs/making-a-progressive-web-app# offline-first-considerations):
The generated service worker doesn't intercept any cross-origin resources like HTTP API requests.
If you decided to go with registering service worker in create react app, you should
register it in index.js
by changing serviceWorker.unregister()
to serviceWorker.register()
.
However, this will not guarantee any updates to be sent to users as the previous service worker
will serve the older content, unless the user hard reloads the browser or close all opened tabs.
What we need is to notify the user that the browser should be hard reloaded, in case he don't want to close all opened tabs,
or even close all tabs opened.
This is the major step in our setup which includes adding:
We will handle service worker state across our PWA by configuring a store that runs only in production environment.
There is a common practice concerning the configuration of redux store which recommends creating store directory into src
home directory. This directory should contain two separate store configurations; one for development environment and another for production.
Here we only need a production configuration that handle the service worker state.
/* connfigure-store.js */
import { createStore } from "redux";
const configureStore = (initialState = { serviceWorkerUpdated: false }) => {
return createStore(/*root reducer*/, initialState);
};
export default configureStore;
In the code snippet above, the initialState
is just an object with a serviceWorkerUpdated
state set to false
and that's it.
We imported the createStore
from redux
passing the initialState
only for now as we will create the root reducer in the next step.
Creating a reducer requires defining actions that aimed to be dispatched for specific input from the app.
In our case, we want to dispatch an action called UPDATE_SERVICEWORKER
once there is any waiting service
worker, informing the user that this app is being served cache-first and there is new content that will
not be shown unless you closed all browser tabs.
/* service-worker-reducer.js */
// CONSTANTS
export const UPDATE_SERVICEWORKER = "UPDATE_SERVICEWORKER";
export function updateServiceWorker() {
return {
type: UPDATE_SERVICEWORKER
};
}
export const reducer = (
state = {
serviceWorkerUpdated: false
},
action
) => {
switch (action.type) {
case UPDATE_SERVICEWORKER: {
return {
...state,
serviceWorkerUpdated: true
};
}
default:
return state;
}
};
If the UPDATE_SERVICEWORKER
action is dispatched, the state of the app will be updated with serviceWorkerUpdated
= true
.
Then, store configurations should be updated passing service-worker-reducer
as the root reducer.
/* configure-store.js */
import { createStore } from "redux";
import { reducer as rootReducer } from "./service-worker-update";
const configureStore = (initialState = { serviceWorkerUpdated: false }) => {
return createStore(rootReducer, initialState);
};
At the current state, we didn't dispatch any actions yet. The question, here, is: when the service worker update action should be dispatched?
Fortunately, there is a service worker implementation shipped with Create React App that has initial control on service worker state changing.
This implementation will be found in registerValidSW
method in service worker file shipped with CRA. This method is invoked once you change serviceWorker.unregister()
to
serviceWorker.register()
, remember?
This method takes the path of the service worker file in the project and config
object which will be used later to carry an update callback function.
You may ask what's the callback function? why I need it?, well, this callback function will dispatch the service worker update action,
and it will be called when there is a service worker update installed and waiting to be activated.
This can be found in the code snippet below imported from CRA serviceWorker.js
.
/* from serviceWorker.js shipped with CRA */
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
"New content is available and will be used when a ll " +
"tabs for this page are closed. See https://bit .ly/CRA-PWA."
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate();
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log("Content is cached for offline use.");
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
};
You see, we can check on config
object with onUpdate
callback.
Then we invoke it. But, what will this function do? it will dispatch the action, finally! :tada:
const onUpdate = () => {
store.dispatch(updateServiceWorker());
};
After all of this, we can add the implementation of the callback function in index.js
and supply a provider that will expose the service worker state in the root component.
We do this by:
ā¢ Wrapping the rendering root component with a Provider
imported from react-redux
passing the store as props.
This will make the store available in all nested components. Intuitively, the store will be available in
the whole app components with the connect()
currying function.
/*from index.js*/
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
ā¢ mapStateToProps
Once service worker state is exposed in the app, it will be needed to extract it in the root component.
We do this by using mapStateToProps
with connect
in the root component.
First we will connect the root component with mapStateToProp
function:
export default connect(mapStateToProps)(App);
Then we will extract the service worker state as app prop:
const mapStateToProps = state => {
return {
serviceWorkerUpdated: state.serviceWorkerUpdated
};
};
Everything tends to have an end when the rendering time comes. we need to push a notification every time the app finish
with rendering all the components proberly and there is an update available. We can get this by using componentDidUpdate
and componentDidMount
or even useEffect
react hook.
In this article I will try useEffect
but it's the same to do it with others.
At this moment, our app has a prop called serviceWorkerUpdated
. It's a boolean, we need to check if it is true in useEffect
function.
If this prop value is true
then a push notification should be displayed else nothing happend
useEffect(() => {
if (props.serviceWorkerUpdated) {
toast(<Msg />, {
position: "bottom-right"
});
}
}, [props.serviceWorkerUpdated]);
I used props.serviceWorkerUpdated
dependency as a second argument to avoid firing this effect when the service worker state hasn't changed.
Also, as you can see I am using react-toastify. I will not dig into any details here, you can find this in their documentation.
The major thing is the message component that will be rendered to the user:
const Msg = ({ closeToast }) => (
<>
<p> Update available, Please refresh your browser!</p>
<div>
<span>From PCs: Press Ctrl + Shift + R </span>
<span>From Mobile Phones: Close all your opened tabs</span>
</div>
</>
);
According to toastify docs this component will not work unless you define a container for it in the app:
/* toast container as child in your app */
<ToastContainer position="bottom-right" toastClassName="toast-container" />
We've created the redux store for the main app exposing the service worker state in our app, then dispatched an update action when there is "updates in the content" flag raised from service worker. Finally, we've extracted this state after the root component rendering finished. If there is any update notify the user else nothing happened.
Source code available on Github
ŁArticle Url: https://syncatallah.cc/writings/notify-pwa-updates
Ahmad Atallah
See other articles by Ahmad
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!