React & Service Worker - Cached Content - Inform user - reactjs

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.)

Related

Update React Component With Updated Data From Firestore

I have a chrome extension that stores data in Firestore and populates that data to the frontend. I always have to refresh the page to see newly added data, which isn’t a user friendly experience. How can I update the UI to show the newly updated data without having to refresh the page?
So far, I've tried using useEffect to get the data. Inside of it, I'm using a function that gets data from Firestore cached inside of chrome local storage.
Here is my code
const getFolderData = () => {
getDataFromChrome("docId").then((res: any) => {
setDocId(res.docId);
});
getDataFromChrome("content").then((res: any) => {
//console.log("getting in mainfolder",res);
// for (const item of res.content) {
// if (item.type.toLowerCase() === "subfolder") {
// // console.log(item)
// getSubFolder(item.id);
// }
// }
for (const item of res.content) {
setTiersContent((pre: any) => [...pre, item]);
}
});
};
useEffect(() => {
getFolderData();
}, []);
I also get this error. I'm also using the chrome extension API to communicate with a background script. It could be related to the problem
Uncaught (in promise) Error: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received
I've never used firebase so I'm not sure what your functions do, I can only guess. A few things wrong from what I can see:
Your useEffect is set to only run on page load since the dep array is empty, I assume you want to refetch on some condition.
If any of the 2 functions is supposed to be a subscription, your useEffect needs to return a cancel function.
Refetch data when needed is not a new problem, packages like React Query has tools that optimize your requests and refetch when needed. I suggest you give it a shot if your app has more than 2-3 fetch requests.

Create React App PWA - change caching strategy of service worker

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

Google One Tap SignIn Popup not showing

I was trying to implement Google One Tap SignIn in my project. At the first time after building the project the google one tap prompt will display. But next time onwards if we refresh the page also the prompt is not displaying.
Here is my code snippet.
import { addScript } from 'Util/DOM';
/**
* Loads One Tap Client Library
*/
const loadOneTapClientLibrary = async() => {
await addScript('https://accounts.google.com/gsi/client');
}
/**
* Loads One Tap Javascript API
* #param {*} resolve
*/
const loadOneTapJsAPI = (resolve) => {
window.onload = () => {
google.accounts.id.initialize({
client_id: "My client Id",
callback: data => resolve(data)
});
google.accounts.id.prompt();
}
}
export const loadOneTap = async() => {
return new Promise( (resolve, reject) => {
loadOneTapClientLibrary();
loadOneTapJsAPI(resolve);
})
}
After page loads i am calling loadOneTap();
To avoid One Tap UI prompting too frequently to end users, if users close the UI by the 'X' button, the One Tap will be disabled for a while. This is the so-called "Exponental Cool Down" feature. More detail at: https://developers.google.com/identity/one-tap/web/guides/features#exponential_cool_down
I believe you triggered this feature during development. To avoid this, use Chrome incognito mode (and restart the browser when necessary).
As noted by Guibin this is the OneTap exponential cool down feature, it can be easily triggered during development when testing auth flow, but also legitimately when the end user clicks the close icon by mistake. On sites where Google login is optional this might seem pragmatic (i.e. the user genuinely wants to dismiss the popup prompt in favor of alternative login methods), however on a site where Google is the sole login identity provider and you are using the Javascript API instead of HTML api then this can manifest as broken functionality - i.e. no login prompt - and you want to avoid telling your users to use incognito or clear cache/cookies at all costs..
You can potentially handle this with some fallback logic..
window.google.accounts.id.prompt((notification) => {
if(notification.isNotDisplayed() || !notification.isDisplayed()) {
// #ts-ignore
const buttonDiv = window.document.createElement("div")
buttonDiv.setAttribute("id", "googleLoginBtn")
document.getElementsByTagName('body')[0].appendChild(buttonDiv);
// #ts-ignore
window.google.accounts.id.renderButton(
document.getElementById("googleLoginBtn"),
{ theme: "outline", size: "large" } // customization attributes
);
}
This renders a button to login that isn't subject the one-tap cool down feature. We're early days into playing with this so there may be other invariants with regards to state you need to consider (e.g. can isNotDisplayed return true when already logged in) - we already observed some oddities where isDisplayed and isNotDisplayed can both be false on the same invocation of the callback.
Extra note: I recall reading the user can disable all one tap features too, so if you're using the javascript API instead HTML api you will need the fallback to SignIn with Google button.

Confirm remote sync - Firebase Realtime Database w/ ReactJS+Redux+Saga

I have a ReactJS/Redux/Saga app which currently sends and reads data from a Firebase Realtime Database. As data is sent and received, there's a global redux state value loading, which toggles between true and false between sending data and confirming that data is now in Firebase. loading defaults to false for this case.
When a user updates their data, the flow is currently:
Redux reducer SEND_TO_FIREBASE
return { ...state, loading: true };
This reducer triggers a Saga function sendToFirebaseSaga()
function* syncToFirebaseSaga({ payload: userData }) {
try {
var uid = firebase.auth().currentUser.uid;
const database = (path, payload) => {
firebase
.database()
.ref(path)
.set(payload);
};
yield call(database, "users/" + uid + "/userData", userData);
yield console.log("successfully written to database");
} catch (error) {
alert(error);
}
}
So, at this point loading:true (confirmed that this works)
Then, as a part of componentDidMount of one of my root components, I have a listener for changes to the Firebase Database:
var props = this.props
function updateStateData(payload, props) {
props.syncFirebaseToState(payload);
}
function syncWithFirebase(uid, props) {
var syncStateWithFirebaseListener = firebase.database().ref("users/" + uid + "/userData");
syncStateWithFirebaseListener.on("value", function(snapshot) {
var localState = snapshot.val();
updateStateData(localState, props);
});
}
and this.props.syncFirebaseToState(payload) is a Redux action with this reducer:
return { ...state, data: action.payload, loading: false };
which then confirms that the data has been written to the Firebase Realtime Database, and then takes down the loading page, letting the user know that their update is now safe.
For most cases, this flow works fine. However, I run into problems when the user has a bad internet connection or if I refresh the page too fast. For example:
User loads app.
Disconnects from internet.
Submits data.
Full loop works immediately and loading:false (Firebase Realtime Database wrote it in 'offline mode' and is waiting to be reconnected to the internet)
User reconnects online.
Once online, user immediately refreshes the page (reloading the React app)
Firebase Realtime Database didn't have time to sync the queued updates to the remote database, and now after page refresh, the edits don't make it.
Sometimes, the user doesn't have to lose their internet connection. If they submit an edit (the page instantly returns a 'successful read') and then refresh before the remote server writes it down, the data is loss after the refresh is complete.
Anyway, as you can see, this is a really bad user experience. I really need a way to confirm that the data has actually been written to Firebase before removing the loading screen. I feel like I must be doing something wrong here and somehow getting a successful callback when it isn't.
This is my first time using React/Redux/Saga/Firebase, so I appreciate the patience and the help!
You could just disable offline mode.
I am assuming you don't want to do that so the next thing is to add a condition to check if your update is coming from the cache or the database.
Firebase Realtime Database provides a special location at /.info/connected which is updated every time the Firebase Realtime Database client's connection state changes. Here is an example:
var connectedRef = firebase.database().ref(".info/connected");
connectedRef.on("value", function(snap) {
if (snap.val() === true) {
alert("connected");
} else {
alert("not connected");
}
});
You can then run this check alongside your update to turn to load off and then propagate the change depending on whether it's coming from cache or the actual database.

Create-react-app reload on service worker update

I want to modify create-react-app service worker file and implement popup message which will ask user to update app if newer service worker is ready to be activated. I'm almost done with the solution but have one pitfall. I want to reload the app when user confirms service worker update popup, so I've added some coded to the end of register function, see below:
export default function register(config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker."
)
})
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl, config)
}
let preventDevToolsReloadLoop
navigator.serviceWorker.addEventListener("controllerchange", function() {
// ensure refresh is called only once
if (preventDevToolsReloadLoop) {
return
}
preventDevToolsReloadLoop = true
console.log("reload")
window.location.reload(true)
})
})
}
}
But the problem is that it reloads the app also on first visit, when there doesn't exist any service worker yet. How can I solve it?
Update to react-scripts ^3.2.0. Verify that you have the new version of serviceWorker.ts or .js. The old one was called registerServiceWorker.ts and the register function did not accept a configuration object. Note that this solution only works well if you are Not lazy-loading.
then in index.tsx:
serviceWorker.register({
onUpdate: registration => {
alert('New version available! Ready to update?');
if (registration && registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
window.location.reload();
}
});
The latest version of the ServiceWorker.ts register()function accepts a config object with a callback function where we can handle upgrading. If we post a message SKIP_WAITING this tells the service worker to stop waiting and to go ahead and load the new content after the next refresh. In this example I am using a javascript alert to inform the user. Please replace this with a custom toast.
The reason this postMessage function works is because under the hood CRA is using workbox-webpack-plugin which includes a SKIP_WAITING listener.
More About Service Workers
good guide: https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68
CRA issue discussing service worker cache: https://github.com/facebook/create-react-app/issues/5316
If you are not using CRA, you can use workbox directly: https://developers.google.com/web/tools/workbox
Completing #jfbloom22's answer:
As you probably want to ask the user after an update has been detected with something more complex than a plain alert, you need to ensure the registration object is available from inside the React's components tree and save it to use after the user accepts to update (for example, by clicking a button).
As an option, in a component you can create a custom event listener on a global object like document and fire this event when the onUpdate callback passed to serviceWorker.register(), passing to it the resgistration object as extra data.
This is exactly what my recently published Service Worker Updater does (some self-promotion). To use it you just need to:
Add it to the dependencies:
yarn add #3m1/service-worker-updater
Use it in your index.js:
import { onServiceWorkerUpdate } from '#3m1/service-worker-updater';
// ...
// There are some naming changes in newer Create React App versions
serviceWorkerRegistration.register({
onUpdate: onServiceWorkerUpdate
});
Use it in some of your React components:
import React from 'react';
import { withServiceWorkerUpdater } from '#3m1/service-worker-updater';
const Updater = (props) => {
const {newServiceWorkerDetected, onLoadNewServiceWorkerAccept} = props;
return newServiceWorkerDetected ? (
<>
New version detected.
<button onClick={ onLoadNewServiceWorkerAccept }>Update!</button>
</>
) : null; // If no update is available, render nothing
}
export default withServiceWorkerUpdater(Updater);
Try to add reload funtion in installingWorker.state === 'installed'
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// do something ..
}
}

Resources