So I have a websocket connection that is open and I notice once I refresh the page the websocket messages disappears. I've read online that this is supposed to happen. What is a good way to persist these messages so that they do not disappear. Right now I have the websocket messages in a react state. I've seen some say localstorage or cookies, but I don't think that is scalable as their can be thousands of messages in minutes that could overload the browser storage? Below I am using react-use-websocket package and I get the last message and store that inside a state array. That is the wrong approach I need a longer storage solution.
const { lastJsonMessage, lastMessage, sendMessage, readyState, getWebSocket } = useWebSocket(resultUrl, {
//Will attempt to reconnect on all close events, such as server shutting down
shouldReconnect: () => true,
reconnectAttempts: 10,
reconnectInterval: 3000
});
useEffect(() => {
const val = lastJsonMessage ? JSON.parse(lastJsonMessage as unknown as string) : {};
if (val !== null && Object.keys(val).length > 0) {
setMessageHistory((prev) => prev.concat(val));
}
}, [lastJsonMessage, setMessageHistory]);
// Get Video assets after finishing;
useEffect(() => {
messageHistory.forEach((msg) => {
const { type } = msg;
if (type === 'video.live_stream.recording' || type === 'video.live_stream.active') {
const localPlaybackId = msg.data?.playback_ids[0];
setPlaybackId(localPlaybackId);
}
setVideoType(type);
});
}, [messageHistory, videoType]);
const connectionStatus = {
[ReadyState.CONNECTING]: 'Connecting',
[ReadyState.OPEN]: 'Open',
[ReadyState.CLOSING]: 'Closing',
[ReadyState.CLOSED]: 'Closed',
[ReadyState.UNINSTANTIATED]: 'Uninstantiated'
}[readyState];
if (liveStreamId) {
sendMessage(liveStreamId);
}
messageHistory.filter((msg) => {
msg.data.id === liveStreamId;
});
in this case, usually when the app is loaded you should do a GET request to the server in order to load all the last messages. Also since you are talking about "thousands of messages" you should implement a lazy loading aka paginator or infinite-scroll
Related
Using expo-splash-screen and SplashScreen.preventAutoHideAsync() throws many errors in production builds and running app on expo go.
I call preventAutoHideAsync first thing in App.js. The warning message is Current activity not found. Make sure to call this method while your application is in foreground.
https://github.com/expo/expo/issues/20502
Implement as per https://docs.expo.dev/versions/v46.0.0/sdk/splash-screen/
Reproducible demo: https://github.com/kirtikapadiya/SplashReactNative
I'm not sure if this will work, but I have the same problem and are gonna try:
// Keep the splash screen visible while we fetch resources
SplashScreen.preventAutoHideAsync().catch((error) => {
// Attach the event to error handling system (Sentry, LogRocket etc.)
// Try to call it again when app state is 'active'
let attempts = 0
const appStateListener = AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'active' && attempts < 0) {
attemps++
SplashScreen.preventAutoHideAsync()
.then(() => appStateListener.remove())
.catch((error) => {
// Attach the event to error handling system (Sentry, LogRocket etc.)
})
}
if (attempts >= 3) {
appStateListener.remove()
}
})
})
export default function App() {
....
[EDIT]
Thanks to #dbuchet for his anwser. The problem comes from the fact that in StrictMode in dev mode only, everythings is running twice.
[PROBLEM]
I just got a weird beavour in React.
My context :
I want to create an EventSource for work with Mercure. So i created a hook and this hook store my EventSource with a useMemo. Until here everything's is fine. But after some tests, eachtime i call my hook, two connections are created not only one.
The problem is that i am not able to close the first connection. Only the second. This cause memory leak and undesired persistent connections.
My useMemo for the EventSource :
const eventSource = useMemo(() => {
if (topics.length === 0 || !hubUrl || !baseUrl) {
return null;
}
let url = new URL(hubUrl);
topics.forEach(topic => {
url.searchParams.append('topic', (baseUrl ? baseUrl : '') + topic);
});
return new EventSource(url, {
withCredentials: withCredentials
});
}, [baseUrl, hubUrl, withCredentials, topicsHash]);
After some investigations, i can see that if i add some Math.random(), logs and a simple setInterval, i am able to see that two different objects are created :
const eventSource = useMemo(() => {
if (topics.length === 0 || !hubUrl || !baseUrl) {
return null;
}
let url = new URL(hubUrl);
topics.forEach(topic => {
url.searchParams.append('topic', (baseUrl ? baseUrl : '') + topic);
});
console.log('opening... ' + temp)
let connectionInterval = null;
connectionInterval = setInterval(() => {
console.log('------------- setInterval ---------------')
onsole.log(topics, eventSource, temp);
clearInterval(connectionInterval);
});
let test = { test: temp, eventSource: new EventSource(url, {
withCredentials: withCredentials
})}
return test;
}, [baseUrl, hubUrl, withCredentials, topicsHash]);
The result in the logger is :
So we can see that my useMemo seems to be called only once (due to the fact that 'opening...' log is visible only once). But the random is different of the final object that i have at the end.
Moreover, setInterval is executed twice with both randoms visible.
Maybe something in my project cause this, so i created a simple peice of React and try the code below and same beavour :
const temp = useMemo(() => {
let test = Math.random();
let inter = setInterval(() => {
console.log('setInterval');
console.log(test);
clearInterval(inter);
});
console.log(test);
}, ['for block reloading'])
Result of the console :
Anyone got the same problem ?
Is there a way to avoid this, maybe i made a mistake somewhere or i misunderstood something with useMemo ?
Thanks !
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
I'm creating an app where two users can watch a youtube video in real time using socket.io. In theory, all of the users will be also to use the video player controls, but an infinite loop occurs if one of the users press play/pause button multiple times in quick succession. How could I prevent this from happening?
Server code:
socket.on("player status", playerStatus => {
socket.broadcast.emit("status", playerStatus)
})
Client-side code:
useEffect(() => {
socketRef.current = io.connect('/');
socketRef.current.on("status", status => {
if (status === 1) {
youtubePlayer.current.playVideo();
} else {
youtubePlayer.current.pauseVideo();
}
})
}, []);
function onPlayerStateChange(e) {
if (e.data === 1 || e.data === 2) {
socketRef.current.emit("player status", e.data)
}
}
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]);