Different WebSocket "onmessage" handlers depending on screen - reactjs

My React Native app (iOS and Android) uses a single global WebSocket connection.
I want to respond to the same messages from the server differently depending on the screen, e.g. one way on the home screen (A) and differently on screen B.
Because the home screen is still mounted and "active" after screen B has been opened, presumably I can't just overwrite the websocket "onmessage" handler as that would lead to inconsistencies.
Can anyone point me in the right direction for what I'm trying to achieve?

Without seeing some code on what you're trying to achieve, I think what you want in general is a subscription model. You have only one handler registered with the socket, but that handler can delegate to other functions that can be added/removed as desired.
In general, I would recommend creating the websocket connection somewhere in the react tree such as a hook with a context provider. To avoid cluttering up the idea here, though, let's assume your websocket connection is defined a static context (i.e. in a module, rather than a component or hook).
// socket.js
const messageHandlers = new Set()
export const addMessageHandler = (handler) => {
messageHandlers.add(handler)
}
export const removeMessageHandler = (handler) => {
messageHandlers.delete(handler)
}
const socket = new WebSocket('ws://example.com')
socket.onmessage = (event) => {
messageHandlers.forEach((handler) => handler(event))
}
Then in your screen:
import { addMessageHandler, removeMessageHandler } from '/path/to/socket.js'
const SomeScreen = () => {
useCallback(() => {
const handler = (event) => { /* do something with event */ }
addMessageHandler(handler)
return () => removeMessageHandler(handler)
}, [])
}
This will keep the listener alive even if the screen is in the background. If you're using react-navigation, you can also make it register only while the screen is the current screen using useFocusEffect such as:
useFocusEffect(
useCallback(() => {
const handler = (event) => { /* do something with event */ }
addMessageHandler(handler)
return () => removeMessageHandler(handler)
}, [])
)

Related

Previous data showing even though cleaning up in useEffect

I have a component in my react native app that loads sessions related to a particular individual. In the useEffect() of that component I both load the sessions when the component comes into focus, and unload those sessions within the cleanup.
export const ClientScreen = (props) => {
const isFocused = useIsFocused();
const client = useSelector((state) => selectActiveClient(state));
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
return () => dispatch(unloadSessions()); // Cleaning up here...
};
loadSessions(props);
}
}, [isFocused, client?.id]);
const updatedProps = {
...props,
client,
};
return <ClientBottomTabNavigator {...updatedProps} />;
};
Generally the component is working as expected. However, I do notice that if I load the component with one client, then navigate away, and then come back to the component by loading a new client, that for a brief moment the sessions pertaining to the previous client show before being replaced the sessions relevant to the new client.
My question is, shouldn't the unloadVisits() that runs on cleanup -- which sets sessions to an empty array -- prevent this? Or is this some kind of react behavior that's holding onto the previous state of the component? How can I ensure this behavior doesn't occur?
Cleanup function should appear before the closing-brace of the useEffect hook
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
};
loadSessions(props);
}
return () => dispatch(unloadSessions()); // Cleaning up here... // <--- here
}, [isFocused, client?.id]);
as commented, your loadSessions returns a cleanup function, but you don't do anything with it. And the effect where you call loadSessions(props) does not return anything, that's why it does not clean up.
Edit:
I made a mistake, loadSessions returns a Promise of a cleanup function. And it is impossible to "unwrap" this Promise and get to the cleanup function itself in a way that you can return it in your effect. You have to move the cleaup function out of the async function loadSessions.
But you don't need async/await for everything:
useEffect(() => {
if (isFocused && client?.id) {
loadClientSessions(client.id).then(dispatch);
return () => dispatch(unloadSessions());
}
}, [isFocused, client?.id]);

How to reset channel of window.echo on changing of route reactjs

I am using laravel-websockets to listen to event. I have no issue on the back-end side; The issue is on the front-end side.
SCENARIO:
When I go to a specific route post/[slug], the current channel is based on the current slug. When I redirect to the same route but different value of slug, the channel listens to the first value on page refresh and not to the current one.
const Component = () => {
const router = useRouter();
const {slug} = router.query;
useEffect(() => {
window.Echo.private(`post.${slug}`).listen('PrivateEvent', e => {
console.log(e)
});
}, [slug])
}
Example:
On page refresh, go to post/first-slug. Next, click to <Link to="/post/second-slug">About</Link>
The example above should listen to second-slug and not the first-slug.
How can I solve this without hard refresh or <a> tag?
You forgot to stop listening on the previous channel, so the events are still received. I suppose that you end up with two active channels, receiving events for both.
Inside a useEffect() you should return a cleanup function that clears resources created for the effect
Here is how:
const Component = () => {
const router = useRouter();
const {slug} = router.query;
useEffect(() => {
window.Echo.private(`post.${slug}`).listen('PrivateEvent', e => {
console.log(e)
});
return () => window.Echo.private(`post.${slug}`).stopListening('PrivateEvent');
}, [slug])
}
If this does not solve your problem, please:
display the slug in your component (return <div>slug</div>;) to confirm that the navigation really happens ;
show us the whole console log.

Best way to detect if a user leaves the page in React (i.e. refresh, close tab, or change url) [duplicate]

I want a function, to send a request to a server (don't care about response) before a user refreshes the page.
I've tried using the componentWillUnmount approach but the page refresh doesn't call this function. e.g.
import React, { useEffect } from 'react';
const ComponentExample => () => {
useEffect(() => {
return () => {
// componentWillUnmount in functional component.
// Anything in here is fired on component unmount.
// Call my server to do some work
}
}, []) }
Does anyone have any ideas how to do this?
You could try listening for the window.beforeunload event.
The beforeunload event is fired when the window, the document and its
resources are about to be unloaded. The document is still visible and
the event is still cancelable at this point.
useEffect(() => {
const unloadCallback = (event) => { ... };
window.addEventListener("beforeunload", unloadCallback);
return () => window.removeEventListener("beforeunload", unloadCallback);
}, []);
Note: This will respond to anything that causes the page to unload though.
Note 2:
However note that not all browsers support this method, and some
instead require the event handler to implement one of two legacy
methods:
assigning a string to the event's returnValue property
returning a string from the event handler.
You can do this, almost, by checking if the page has been refreshed.
Source: https://developer.mozilla.org/en-US/docs/Web/API/Navigation_timing_API
Example:
if (window.performance) {
console.info("window.performance is supported");
console.info(performance.navigation.type);
if (performance.navigation.type == performance.navigation.TYPE_RELOAD) {
console.info( "This page is reloaded" );
} else {
console.info( "This page is not reloaded");
}
}

socket.on listen too much

I created a socket in store (socket.store.ts)
constructor() {
this.socket = io.connect(process.env.REACT_APP_SOCKET_URI || '3131');
}
Using that socket in my component. When user click find button, socket emit one time (checked) to backend, backend emit one time (checked) back to event 'finding'. But called printed into console 12 times and even increase rapidly when i re-click find button
const socketStore = useContext(SocketStoreContext);
const socket = socketStore.socket;
// THOUGHT: maybe i will try to create socket inside this component
// const socket = io.connect(process.env.REACT_APP_SOCKET_URI);
socket.on('finding', () => {
console.log('called');
});
const handleFindPartner = () => {
socket.emit('find', {
token: user.token,
});
};
But when I create socket inside my component, called printed exactly one time. Don't know what going on here, really need some help. Thank you you guys so much!
Like #Mike said in comment section. I need to prevent socket.on from re-rendering by putting it in useEffect
const socketStore = useContext(SocketStoreContext);
const socket = socketStore.socket;
// THOUGHT: maybe i will try to create socket inside this component
// const socket = io.connect(process.env.REACT_APP_SOCKET_URI);
React.useEffect(()=>{
socket.on('finding', () => {
console.log('called');
});
},[socket])
const handleFindPartner = () => {
socket.emit('find', {
token: user.token,
});
};

How to update a React/Recoil state array from web bluetooth subscription callbacks?

I am new to using React and Recoil and want to display real-time charts (using D3) from data that is gathered in real-time using the Web Bluetooth API.
In a nutshell, after calling await myCharacteristic.startNotifications() and myCharacteristic.addEventListener('characteristicvaluechanged', handleNotifications), the handleNotifications callback is called each time a new value is notified from a Bluetooth device (see this example).
I am using hooks and tried to modify a recoil state from the callback (this was simplified to the extreme, I hope it is representative):
export const temperatureState = atom({
key: 'temperature',
default: 0
})
export function BluetoothControls() {
const setTemperature = useSetRecoilState(temperatureState);
const notify = async () => {
...
temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
setTemperature(event.target.value.getInt16(0))
}
}
return <button onClick={nofity}/>Start notifications</button>
}
This work fine if I want to display the latest value somewhere in the app. However, I am interested in keeping the last few (let's say 10) values in a circular buffer to draw a D3 chart.
I tried something along the lines of:
export const temperatureListState = atom({
key: 'temperature-list',
default: []
})
export function BluetoothControls() {
const [temperatureList, setTemperatureList] = useRecoilState(temperatureListState);
const notify = async () => {
...
temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
let temperatureListCopy = temperatureList.map(x => x);
temperatureListCopy.push(event.target.value.getInt16(0))
if (temperatureListCopy.length > 10)
temperatureListCopy.shift()
setTemperatureList(temperatureListCopy)
}
}
return <button onClick={nofity}/>Start notifications</button>
}
However, is is pretty clear that I am running into the issue described here where the function is using an old version of temperatureList that is captured during render. As a result, temperatureState is always empty and then replaced with a list of one element.
How to maintain a consistent list in a React state/Recoil atom that is updated from an external callback? I think this issue is a bit similar but I'd like to avoid using another extension like Recoil Nexus.
useSetRecoilState accepts an updater function as an argument with the value to be updated as the first parameter:
export function BluetoothControls() {
const setTemperatureList = useSetRecoilState(temperatureListState);
const notify = async () => {
...
temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
setTemperatureList(t => {
let temperatureListCopy = t.map(x => x);
temperatureListCopy.push(event.target.value.getInt16(0))
if (temperatureListCopy.length > 10)
temperatureListCopy.shift()
return temperatureListCopy
})
}
}
return <button onClick={nofity}/>Start notifications</button>
}
This solves the issue as the updater function is only evaluated on events.

Resources