Create React App PWA - change caching strategy of service worker - reactjs

I have created a PWA template using CRA v4 and enabled the service worker that comes with it by registering it, because I needed to create a pop up notification about installing the PWA.
The lighthouse test has to pass for the app to be PWA compatible so that the browser would fire the beforeinstallprompt event listener needed to detect if the user has already installed the PWA or not.
The problem now is that this service worker is using cache-first strategy. As a result refreshing the page does not trigger an update and I am left with an older version of the app appearing after I have deployed an update.
How can I change the caching strategy of CRA v4's service worker such that the user would get a new version of the app by simply refreshing the page?
I am also interested in knowing why this cache-first strategy is used by default. To me it seems bad that the user has to close every tab to get a new version. Why haven't more people brought this up? This is clearly not user friendly...

To change the strategy you need to implement your own code changing service-worker.js and potentially
serviceWorkerRegistration.js. (https://developers.google.com/web/tools/workbox/modules/workbox-strategies)
I implement my own strategy:
check for updates at the very beginning check for updates each 3 min if there is a update in the very beginning update
cache and refresh the website. if it is after show a popup asking to fresh
serviceWorkerRegistration.ts
/**
* ...... previous code
**/
const CHECK_INTERVAL_TIME = 1000 * 60 * 3 // 3 min
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
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 all ' +
'tabs for this page are closed. See https://cra.link/PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.info('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
/****************************
start new code
*****************************/
registration.update().then(() => console.debug("Checked for update...")).catch(console.error)
setInterval(() => {
console.debug("Checked for update...");
registration.update().catch(console.error);
}, CHECK_INTERVAL_TIME);
/****************************
end new code
*****************************/
})
.catch((error) => {
console.error('Error during service worker registration:', error);
});
}
App.tsx
/**
* ...... previous code
**/
function App() {
const time = useRef(Date.now()); //can be let, depending of your logic
useEffect(() => {
serviceWorkerRegistration.register({
onSuccess(registration) {
console.debug('serviceWorkerRegistration success')
},
onUpdate(registration) {
console.debug('serviceWorkerRegistration updated',Date.now()-time.current)
const refresh=async ()=>{
await registration?.waiting.postMessage({type: 'SKIP_WAITING'}); //send message to update the code (stop waiting)
if ('caches' in window) { //delete cache, i think is no necessary but you lose nothing
const names = await caches.keys()
for (const name of names) {
await caches.delete(name)
}
}
window.location.reload();
}
if (Date.now()-time.current<=2000){
return refresh()
}
logicToShowPopup({
onClick: refresh
})
}
})
}, [])
return (<div>My App</div>)
}
I hope this suits for your needs

Related

electron + react + firebase enableIndexedDbPersistence show error prompt

electron + react + firebase enableIndexedDbPersistence show error prompt, but working seems normal.
I don't have 10 reputation, so I can't post images, please click on the link, thank you very much for your efforts.
FYI.
error message as follow:
[2022-03-30T05:34:53.552Z]
#firebase/firestore: Firestore (9.6.7): IndexedDbPersistence LocalStorage is unavailable.
As a result, persistence may not work reliably.
In particular enablePersistence() could fail immediately after refreshing the page.
Code as follow:
this.app = initializeApp(firebaseAppConfig);
this.firestoreDB = getFirestore(this.app);
this.realtimeDB = getDatabase(this.app);
const enableOffline = async () => {
try {
await enableIndexedDbPersistence(this.firestoreDB);
} catch (err: any) {
if (err.code == 'failed-precondition') {
logger.error('failed-precondition', err);
// Multiple tabs open, persistence can only be enabled
// in one tab at a a time.
// ...
} else if (err.code == 'unimplemented') {
logger.error('unimplemented', err);
// The current browser does not support all of the
// features required to enable persistence
// ...
} else {
logger.error('unkown', err);
}
}
};
enableOffline();

Role of navigator.serviceWorker.controller in serviceWorker.js of Create React App

Below is an excerpt of serviceWorker.js generated by CRA.
What is the point of having if (navigator.serviceWorker.controller) inside
if (installingWorker.state === 'installed')?
The new service worker has been successfully installed at this stage. Does the 'else' part handle the scenario in which no previous service worker has been installed yet?
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
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 all ' +
'tabs for this page are closed.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} 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);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
navigator.serviceWorker.controller will be set to a truthy value (a ServiceWorker object) if the current page is under the control of a service worker.
If this is the first time a service worker's been registered whose scope encompasses the current page's URL, then navigator.serviceWorker.controller will not be set (unless the service worker itself calls clients.claim()).
If this is a subsequent visit to a page that is under the registered service worker's scope, then navigator.serviceWorker.controller will be set, and the page will be under the service worker's control.

How to activate a react route and pass data from the service worker?

I have a SPA PWA React app.
It is installed and running in standalone mode on the mobile device (Android+Chrome).
Let's say the app lists people and then when you click on a person it diplays details using /person route.
Now, I'm sending push notifications from the server and receiving them in the service worker attached to the app. The notification is about a person and I want to open that person's details when the user clicks on the notification.
The question is:
how do I activate the /person route on my app from the service worker
and pass data (e.g. person id, or person object)
without reloading the app
From what I understand, from the service worker notificationclick event handler I can:
focus on the app (but how do I pass data and activate a route)
open an url (but /person is not a physical route, and either way - I want avoid refreshing the page)
You can listen for click event for the Notification which you show to the user. And in the handler, you can open the URL for the corresponding person which comes from your server with push event.
notification.onclick = function(event) {
event.preventDefault();
// suppose you have an url property in the data
if (event.notification.data.url) {
self.clients.openWindow(event.notification.data.url);
}
}
Check these links:
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/notificationclick_event
https://developer.mozilla.org/en-US/docs/Web/API/Clients/openWindow
To answer my own question: I've used IndexedDB (can't use localStorage as it is synchronous) to communicate between SW and PWA, though I'm not too happy about it.
This is roughly how my service worker code looks (I'm using idb library):
self.addEventListener('notificationclick', function(event) {
const notif = event.notification;
notif.close();
if (notif.data) {
let db;
let p = idb.openDB('my-store', 1, {
upgrade(db) {
db.createObjectStore(OBJSTORENAME, {
keyPath: 'id'
});
}
}).then(function(idb) {
db = idb;
return db.clear(OBJSTORENAME);
}).then(function(rv) {
return db.put(OBJSTORENAME, notif.data);
}).then(function(res) {
clients.openWindow('/');
}).catch(function(err) {
console.log("Error spawning notif", err);
});
event.waitUntil(p);
}
});
and then, in the root of my react app ie in my AppNavBar component I always check if there is something to show:
componentWillMount() {
let self = this;
let db;
idb.openDB('my-store', 1)
.then(function (idb) {
db = idb;
return db.getAll(OBJSTORENAME);
}).then(function (items) {
if (items && items.length) {
axios.get(`/some-additional-info-optional/${items[0].id}`).then(res => {
if (res.data && res.data.success) {
self.props.history.push({
pathname: '/details',
state: {
selectedObject: res.data.data[0]
}
});
}
});
db.clear(OBJSTORENAME)
.then()
.catch(err => {
console.log("error clearing ", OBJSTORENAME);
});
}
}).catch(function (err) {
console.log("Error", err);
});
}
Have been toying with clients.openWindow('/?id=123'); and clients.openWindow('/#123'); but that was behaving strangely, sometimes the app would stall, so I reverted to the IndexedDB approach.
(clients.postMessage could also be the way to go though I'm not sure how to plug that into the react framework)
HTH someone else, and I'm still looking for a better solution.
I had a similar need in my project. Using your's postMessage tip, I was able to get an event on my component every time a user clicks on service worker notification, and then route the user to the desired path.
service-worker.js
self.addEventListener("notificationclick", async event => {
const notification = event.notification;
notification.close();
event.waitUntil(
self.clients.matchAll({ type: "window" }).then(clientsArr => {
if (clientsArr[0]) {
clientsArr[0].focus();
clientsArr[0].postMessage({
type: "NOTIFICATION_CLICK",
ticketId: notification.tag,
});
}
})
);
});
On your react component, add a new listener:
useEffect(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", message => {
if (message.data.type === "NOTIFICATION_CLICK") {
history.push(`/tickets/${message.data.ticketId}`);
}
});
}
}, [history]);

Event listener for window.stripe

I have a Stripe integration in a React web front-end. My payment component attempts to run…
componentDidLoad() {
if ( window.hasOwnProperty('Stripe') ) {
this.setState({stripe: window.stripe(config.stripeKey)})
}
}
This sporadically fails as sometimes the React app will boot and get the component loaded before window.stripe exists. I’m currently resolving this with…
componentDidLoad() {
this.watchStripe = setInterval( () => {
if (!props.stripe && isBrowser && window.Stripe) {
this.setState({stripe: window.stripe(config.stripeKey)})
clearInterval(this.watchStripe)
}
}, 100)
}
Is there a more elegant solution to this, something that doesn't require an interval timer?
If you read the Advanced integrations section they suggest doing
componentDidMount() {
if (window.Stripe) {
this.setState({stripe: window.Stripe('pk_test_12345')});
} else {
document.querySelector('#stripe-js').addEventListener('load', () => {
// Create Stripe instance once Stripe.js loads
this.setState({stripe: window.Stripe('pk_test_12345')});
});
}
}
This adds an event listener for the load event on the script element that loads Stripe and sets the state when it is available (so no continuous polling with setInterval,setTimeout is needed)

React & Service Worker - Cached Content - Inform user

I have a question regarding service workers and reactjs.
The recommendation is to inform the user about cached content or when new content is available, so that the user knows about cached content.
My question is now, how can I inform the user?
When I use create-react-app, in the registerServiceWorker.js there is this code, where it says:
At this point, the old content will have been purged and the
fresh content will have been added to the cache. It's the perfect
time to display a "New content is available; please refresh."
message in your web app.
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} 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.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
But actually on this script, of course, I do not have access to the document, because the service worker works away from the script.
How can I handle this in an react component?
How do others handle this issue and inform the user?
The code included in your question runs inside the context of the window client. You have full control over showing whatever UI elements you'd like. Feel free to modify those console.log() statements and display something instead.
(There's separate code that runs in the context of the service worker, and you're correct that that code doesn't have access to the DOM. But that's not the code you're asking about.)

Resources