React SocketContext: 2 Components using 1 Websocket - reactjs

MY PROBLEM
I have a server which receives streaming data over UDP and rebroadcasts the data over websockets. In my front-end I have 2 different components which pick up the data and render the values. Whenever my server broadcast the object, key A is picked up by component 1. And key B should be picked up by component B.
For debugging purposes, the code below works smooth if I were to use a single component, but as soon as I use both only 1 is able to fetch and update data correctly while the other does nothing. What am I doing wrong?
MY SERVER (Just the important stuff)
# Dict to hold values
var data = {"score" : 0, "session" : 0,}
# handle different incoming UDP packets
client.on(PACKETS.session, handle_session);
client.on(PACKETS.score, handle_score);
#update data.session object with new values
function handle_session(data) {
data['session'] = data.session
websocket.broadcast(data);
}
#update data.score object with new values
function handle_score(data) {
data['score'] = data.score
websocket.broadcast(data);
}
# Broadcast data function gets called in handle functions.
websocket.broadcast = function broadcast(data) {
websocket.clients.forEach(function each(client) {
console.log(data);
client.send(data);
});
};
MY REACT APP
CONTEXT.JS - Create my socket
export const socket = new WebSocket("ws://localhost:8000")
export const SocketContext = React.createContext();
APP.JS - I wrap both my components in SocketContext
function App() {
return (
<div className="App">
<SocketContext.Provider value={socket}>
< Score />
< Session />
</SocketContext.Provider>
</div>
SCORE.JS - Component number 1 loads the socket from context.
function Score() {
const [score, setScore] = useState(0)
const websocket = useContext(SocketContext);
useEffect(() => {
websocket.onmessage = function(e) {
var packet = JSON.parse(e.data);
setScore(packet.score)
}
}, [websocket,]);
return (
<h6>{score}</>
);
}
export default Score;
SESSION.JS - Component number 2 loads the same socket from context.
function Session() {
const [session, setSession] = useState(0)
const websocket = useContext(SocketContext);
useEffect(() => {
websocket.onmessage = function(e) {
var packet = JSON.parse(e.data);
setSession(packet.session)
}
}, [websocket,]);
return (
<h6>{session}</>
);
}
export default Session;
So when running the frontend and adding console logging in useEffect for both components, Only one runs at the time. Then when reloading the app. Another one runs. They never run both at the same time.

Do you have any specific error messages in the console?
This bit on the server-side seems problematic:
data['score'] = {data.score}
It seems that should be:
data['score'] = data.score;
Edit*
Also another issue I see is variable scope overwriting:
You set:
var data = {"score" : 0, "session" : 0,}
But later both your functions use data in their local scope which is not good and would cause issues:
function handle_session(data) {
Should probably be:
function handle_session(d) {
Also this:
websocket.broadcast = function broadcast(data) {
Should probably be:
websocket.broadcast = function broadcast(d) {
These functions can share the same inner scope variable, d because they are siblings.
***EDIT, UPDATED ANSWER
The issue is with the use of websocket.onmessage. Since websocket is a single instance shared between components, once the listener is set on the onmessage property inside App.js, subsequent children components will be ignored when they try to set the same listener on the same property on the same websocket instance.
The key is to use addListener instead!
https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event
I created a simple working React <> Express <> Websocket App example of your situation, resolved by using addListener. I reproduced your exact problem.
Now all 3 component's useEffects will properly listen as you expected.
Open a terminal and run: npm run dev
https://codesandbox.io/p/sandbox/bitter-mountain-q3x118
Hope that helps!

Related

Mapping State in React with Server Requests

I'm new to React as we are trying to migrate our app from AngularJS. One thing I'm struggling to wrap my head around is what's the best way to make and cache state mapping requests.
Basically, I would do a search, that returns a list of objects and one of the field is a status code (e.g. 100, 200, 300, etc.), some number. To display the result, I need to map that number to a string and we do that with a http request to the server, something like this:
GET /lookup/:stateId
So my problem now is:
I have a list of results but not many different states, how can I make that async call (useEffect?) to make that lookup only once for different stateId? Right now, I can get it to work, but the request is made on every single mapping. I'm putting the Axio call in a utility function to try and reuse this across multiple pages doing similar things, but is that the "React" way? In AngularJS, we use the "|" filter to map the code to text.
Once I have that mapping id => string, I want to store it in cache so next one that needs to map it no longer make the http request. Right now, I put the "cache" in the application level context and use dispatch to update/add values to the cache. Is that more efficient? It appears if I do a language change, where I keep the language in the same application context state, the cache would be re-initialized, and I'm not sure what other things would reset that. In AngularJS, we used the $rootState to 'cache'.
Thanks for any pointers!
In a lookupUtil.js
const DoLookupEntry = async (entryId) => {
const lookupUrl = `/lookup/${entryId}`;
try {
const response = await Axios.get(looupUrl,);
return response.data;
} catch (expt) {
console.log('error [DoLookupEntry]:',expt);
}
}
In a formatUtils.js
const formatLookupValue = (entryId) => {
const appState = useContext(AppContext);
const appDispatch = useContext(DispatchContext);
const language = appState.language;
if (appState.lookupCache
&& appState.lookupCache[entryId]
&& appState.lookupCache[entryId][language]) {
// return cached value
const entry = appState.lookupCache[entryId][language];
return entry.translatedValue;
}
// DoLookup is async, but we are not, so we want to wait...
DoLookupEntry(entryId)
.then((entry) => { // try to save to cache when value returns
appDispatch({type: States.APP_UPDATE_LOOKUP_CACHE,
value:{language, entry}})
return entry.translatedValue;
});
}
And finally the results.js displaying the result along the line (trying formatLookupValue to map the id):
{searchState.pageResults.map((item) => {
return (
<tr>
<td><Link to={/getItem/item.id}>{item.title}</Link></td>
<td>{item.detail}</td>
<td>{formatLookupValue(item.stateId)}</td>
</tr>
)
})}

How to use ethers.js-JsonRpcProvider with Next.js to retrieve onchain data?

I'm trying to read some on-chain data with ethers.js JsonRpcProvider. I'm calling it with an Alchemy key, stored in a .env file.
edit : Making this variable only visible to server side is intentionnal, because I do not want to share this "sensible" information. Maybe it's not the right way to do this, I don't know...
I can't load my data on the client side, but on the server side, everything seems fine :/
What I'm doing wrong?
// ALCHEMY_API_KEY_URL is in .env file
const passiveProvider = new ethers.providers.JsonRpcProvider(process.env.ALCHEMY_API_KEY_URL)
// everything's fine on server side
console.log(passiveProvider)
const whiteListContract = new ethers.Contract(WHITELIST_CONTRACT_ADDRESS, abi, passiveProvider)
export default function Home() {
const [value, setValue] = React.useState("")
React.useEffect(() => {
async function getNumAddressesWhitelisted(contract) {
try {
// we're in useEffect, client-side, nothing works here :/
// dunno if this executes
const clientTx = await contract.numAddressesWhitelisted()
console.log("numAddressesWhitelisted : ", clientTx)
setValue(clientTx)
} catch (err) {
// we drop here each time
console.log("there is an error ! ",err)
// error message dropped :
// there is an error ! Error: could not detect network (event="noNetwork", code=NETWORK_ERROR, version=providers/5.7.0)
// at Logger.makeError (index.js?dd68:224:1)
// at Logger.throwError (index.js?dd68:233:1)
// at JsonRpcProvider.eval (json-rpc-provider.js?8679:442:1)
// at Generator.throw (<anonymous>)
// at rejected (json-rpc-provider.js?8679:6:42)
}
}
getNumAddressesWhitelisted(whiteListContract)
}, [])
return (
<div>
{value}
</div>
)
}
I think this line with the env variable is the issue:
const passiveProvider = new ethers.providers.JsonRpcProvider(process.env.ALCHEMY_API_KEY_URL)
Because environment variables that are inside .env file loaded on the server but not on the client. so passiveProvider is successfully defined on server side but client-side. In order to that env variable work on the client side, you have to add NEXT_PUBLIC_ prefix to the naming
NEXT_PUBLIC_ALCHEMY_API_KEY_URL=...........
now this env variable will be defined on the client side
const passiveProvider = new ethers.providers.JsonRpcProvider(process.env.NEXT_PUBLIC_ALCHEMY_API_KEY_URL)
What's the difference between exposing environment variables in nextjs through the next.config.js vs with the NEXT_PUBLIC prefix?
Ok, I think I've figured out a righter/cleaner way to do this by far :
I needed getServerSideProps. getServerSideProps can only exist in a next.js page, executes code on server side only, and returns props passed to the page.
The logs appears only on server side too, as expected :
export async function getServerSideProps() {
// ALCHEMY_API_KEY_URL is defined in .env file, it's visible only from server side
const passiveProvider = new ethers.providers.JsonRpcProvider(process.env.ALCHEMY_API_KEY_URL)
console.log("passive provider : ", passiveProvider)
const whiteListContract = new ethers.Contract(WHITELIST_CONTRACT_ADDRESS, abi, passiveProvider)
const clientTx = await whiteListContract.numAddressesWhitelisted()
console.log("numAddressesWhitelisted : ", clientTx)
return { props: { clientTx } }
}
export default function Home({ clientTx }) {
return (
<div>
{clientTx}
</div>
)
}

Subscribing to the socket uses the state of the variable as empty. After Updating the state it's not using the state after updating

I'm trying to connect to a socket endpoint that sends a JSON object every time either of the users sends messages back and forth.
I'm connecting to the socket and subscribing to a topic. Whenever the message is received it needs to add the object to the state. Since I'm using useEffect to connect the socket, it is called first and the socket gets subscribed to the topic with the empty state. So whenever a message is received, it is getting added to the empty array resulting in only one object.
Here's my code,
The socket code
const [chatData,setChatData] = useState([]);
function connect() {
var socket = new SockJS(""); //I've connected to my backend socket
console.log(chatData);
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
isConnected = true;
console.log("Connected: " + frame);
stompClient.subscribe(
"/topic/oneToOneChat/" + user.id,
function (message) {
const m = JSON.stringify(message.body);
const socketMessage = JSON.parse(JSON.parse(m));
chatData.unshift(socketMessage); //Adding the object to the top of the array
setChatData([...chatData]); //This is adding the object to the empty array
}
);
stompClient.subscribe("/topic/oneToOneChat/" + id, function (message) {
const m = JSON.stringify(message.body);
const socketMessage = JSON.parse(JSON.parse(m));
chatData.unshift(socketMessage);
setChatData([...chatData]);
});
stompClient.subscribe(
"/topic/deleteOneToOneChat/" + id,
function (message) {
const m = JSON.stringify(message.body);
const socketMessage = JSON.parse(JSON.parse(m));
deleteMessageFromChatData(socketMessage.id);
}
);
stompClient.subscribe(
"/topic/deleteOneToOneChat/" + user.id,
function (message) {
const m = JSON.stringify(message.body);
const socketMessage = JSON.parse(JSON.parse(m));
deleteMessageFromChatData(socketMessage.id);
}
);
});
}
useEffect(() => {
connect();
}, [chatData]);
chatData is my state where I update my array of objects. After updating all the chats are showing on my screen. But when I send or receive the message, the socket is adding the message object to the chatData and removing all the previous messages. Since it is appending to the empty chatData.
I've solved this by adding the below lines inside the socket subscription
setChatData((chats) => [socketMessage,...chats])

How can I call a function from Chrome console?

I have react component:
const App = ()=> {
function ASD() {
alert("ASD");}}
I want in Chrome console type ASD(); and get the alert.
Just add it to the global window object.
function ASD() {
alert('ASD');
}
window.ASD = ASD;
const App = () => {};
console
ASD(); // or
window.ASD();
Presuming - your code might be looking like following, since the one OP has provided, is not a valid react-component -
const App = ()=> {
function ASD() {
alert("ASD");
}
window.ASD = ASD; // add this to read `ASD` from console by using "window.global"
return null; // this was at-least missing for the App to be a valid functional component
}
OP wants to call ASD from console(developer-console in Chrome). That could be done by attaching the function to the window global variable - like done in the above code.

Firebase function not executed

import * as functions from 'firebase-functions';
console.log('I am a log entry0!');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
console.log('I am a log entry1!');
export const onMessageCreate = functions.database
.ref('/users/{userId}/totalScore')
.onUpdate((change) => {
console.log('I am a log entry2!');
//var a = admin.firestore().collection('/users');
})
I have deployed the function and I can see it in the console. But the function is not executed when totalScore is updated in the database....
Your database is Firestore but you use a Cloud Function that is triggered by an update in the Realtime Database. These are two different Firebase services and you need to change your code accordingly.
The following code will work:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.onMessageCreate = functions.firestore
.document('users/{userId}')
.onUpdate((change, context) => {
// Get an object representing the document
const newValue = change.after.data();
// ...or the previous value before this update
const previousValue = change.before.data();
if (newValue.totalScore !== previousValue.totalScore) {
console.log('NEW TOTAL SCORE');
}
return null;
//I guess you are going to do more than just logging to the console.
//If you do any asynchronous action, you should return the corresponding promise, see point 3 below
//For example:
//if (newValue.totalScore !== previousValue.totalScore) {
// return db.collection("score").doc(newValue.name).set({score: newValue.totalScore});
//}
});
Note that:
You cannot trigger the onUpdate Cloud Function when a specific field of the document changes. The Cloud Function will be triggered when any field of the Firestore document changes. But you can detect which field(s) have changed, as shown in the above code.
Since version 1.0 you have to initialize with admin.initializeApp();, see https://firebase.google.com/docs/functions/beta-v1-diffList
You need to indicate to the platform when the Cloud Function has finished executing: Since you are not executing any asynchronous operation in your Cloud Function you can use return null;. (For more details on this point, I would suggest you watch the 3 videos about "JavaScript Promises" from the Firebase video series: https://firebase.google.com/docs/functions/video-series/).
I think the update is checked on the ref not on the child
Try this
export const onMessageCreate = functions.database
.ref('/users/{userId}')
.onUpdate((change) => {
console.log('I am a log entry2!');
//var a = admin.firestore().collection('/users');
})
You get the old and new values of the snapshot at that location
If you are using Cloud Firestore then your listener is incorrect. In your case, you are specifying a listener for Realtime Database. We extract firestore from the functions and specify the path to the document we want to have a listener on:
import * as functions from 'firebase-functions';
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
export const onMessageCreate = functions.firestore
.document('users/{userId}')
.onUpdate((change, context) => {
console.log(change.before.data()); // shows the document before update
console.log(change.after.data()); // shows the document after change
return;
})

Resources