React with Socket.io first request in useEffect - reactjs

I am using Socket.io in react app. I would like to do first request immediately after app loads so I put my first emit in useEffect hook:
useEffect(() => {
socket.emit("getCollectionsAndVolumes", socket.id);
}, []);
but it doesn't work. It does not do any request. So I realized that a problem is because maybe socket is not connected yet so I put little timeout like:
useEffect(() => {
const initialGetTimeout = setTimeout(() => {
clearTimeout(initialGetTimeout);
}, 1000);
})
and it works but it is only a workaround, how should I trigger first request right after app loads? Is there any other way to do that in React?

Add a parameter which gets changed when socket is connected as a dependency for useEffect React.useEffect(()=>{},[dependencies]);, if there is no such parameter, then try creating a state that maintains if socket is connected or not.

I fixed that by using:
socket.on("connect", () => {
socket.emit("getCollectionsAndVolumes", socket.id);
});
so when it is true it immediately executes request

Try to monitor socket with useEffect then add 'connect' listener. Inside that listener, create a callback function to emit first request.
// Websocket Listener
useEffect(() => {
const handleCode = (res) => {
setCode(res.code)
}
// Listeners
socket.on('res_mvp_code', handleCode)
// Join room
socket.on('connect', () => {
socket.emit('req_mvp_join', { room: 'mvp-1' }) // this is your first request
})
// Unlisteners
return () => {
socket.off('res_mvp_code', handleCode)
}
}, [socket])

Related

Using useEffect properly when making reqs to a server

I have a handleRating function which sets some state as so:
const handleRating = (value) => {
setCompanyClone({
...companyClone,
prevRating: [...companyClone.prevRating, { user, rating: value }]
});
setTimeout(() => {
handleClickOpen();
}, 600);
};
I think also have a function which patches a server with the new companyClone values as such:
const updateServer = async () => {
const res = await axios.put(
`http://localhost:3000/companies/${companyClone.id}`,
companyClone
);
console.log("RES", res.data);
};
my updateServer function gets called in a useEffect. But I only want the function to run after the state has been updated. I am seeing my res.data console.log when I load my page. Which i dont want to be making reqs to my server until the comapanyClone.prevRating array updates.
my useEffect :
useEffect(() => {
updateServer();
}, [companyClone.prevRating]);
how can I not run this function on pageload. but only when companyClone.prevRating updates?
For preventing function call on first render, you can use useRef hook, which persists data through rerender.
Note: useEffect does not provide the leverage to check the current updated data with the previous data like didComponentMount do, so used this way
Here is the code example.
https://codesandbox.io/s/strange-matan-k5i3c?file=/src/App.js

How to use context values in useEffect, that only runs once

i've got an interesting problem here. I am building a react application using web socket communication with the server. I create this websocket in a useEffect hook, which therefore cannot run multiple times, otherwise i'd end up with multiple connections. In this useEffect, however i intend to use some variables,which are actually in a context (useContext) hook. And when the context values change, the values in useEffect , understandably, don't update. I've tried useRef, but didn't work. Do you have any ideas?
const ws = useRef<WebSocket>();
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
ws.current.addEventListener("message", (message) => {
const messageData: ResponseData = JSON.parse(message.data);
const { response, reload } = messageData;
if (typeof response === "string") {
const event = new CustomEvent<ResponseData>(response, {
detail: messageData,
});
ws.current?.dispatchEvent(event);
} else {
if (reload !== undefined) {
console.log("general info should reload now");
GeneralInfoContext.reload(reload);
}
console.log(messageData);
}
});
});
The web socket is stored as a ref for better use in different functions outside of this useEffect block
Note: the context value to be used is actually a function, GeneralInfoContext.reload()
Solution with split useEffect
You can split the logic that opens the websocket connection vs. the one that adds the message handler into separate useEffects - the first can run once, while the second can re-attach the event every time a dependency changes:
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
}, []);
useEffect(() => {
const socket = ws.current;
if(!socket) throw new Error("Expected to have a websocket instance");
const handler = (message) => {
/*...*/
}
socket.addEventListener("message", handler);
// cleanup
return () => socket.removeEventListener("message", handler);
}, [/* deps here*/])
The effects will run in order so the second effect will run after the first effect has already set ws.current.
Solution with callback ref
Alternatively you could put the handler into a ref and update it as necessary, and reference the ref when calling the event:
const handlerRef = useRef(() => {})
useEffect(() => {
handlerRef.current = (message) => {
/*...*/
}
// No deps here, can update the function on every render
});
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
const handlerFunc = (message) => handlerRef.current(message);
ws.current.addEventListener("message", handlerFunc);
return () => ws.current.removeEventListener("message", handlerFunc);
}, []);
It's important that you don't do addEventListener("message", handlerRef.current) as that will only attach the original version of the function - the extra (message) => handlerRef.current(message) wrapper is necessary so that every message gets passed to the latest version of the handler func.
This approach still requires two useEffect as it's best to not put handlerRef.current = /* func */ directly in the render logic, as rendering shouldn't have side-effects.
Which to use?
I like the first one personally, detaching and reattaching event handlers should be harmless (and basically 'free') and feels less complicated than adding an additional ref.
But the second one avoids the need for an explicit dependency list, which is nice too, especially if you aren't using the eslint rule to ensure exhaustive deps. (Though you definitely should be)
You can provide useEffect with a list of variables and useEffect will re-run when these variables change.
This is a little example:
const [exampleState, setExampleState] = useState<boolean>(false);
useEffect(() => {
console.log("exampleState was updated.");
}, [exampleState]);
An example from reactjs website:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes
You should pass an empty array as the second parameter to the useEffect, so it this case it becomes akin to the componentDidMount() logic of react
useEffect(() => {
...your websocket code here
}, [])

React: Execute function on website exit, without giving an alert warning

I have a hook that executes the function passed to it when someone leaves the current page, or when they leave the website
const useOnPageLeave = (handler) => {
useEffect(() => {
window.onbeforeunload = undefined;
window.addEventListener('beforeunload', (event) => { //When they leave the site
event.preventDefault(); // Cancel the event as stated by the standard.
handler();
});
return () => {
handler(); //When they visit another local link
document.removeEventListener('beforeunload', handler);
};
}, []);
};
When using Firefox or Safari, this alert message appears(only when they leave the site, not when they visit a local link), and I don't want this alert message to show
If I remove the line event.preventDefault(), then the handler function is not executed, and I need the handler event to be executed
Here is an MRE of the problem
const useOnPageLeave = (handler) => {
useEffect(() => {
window.onbeforeunload = () => handler();
window.addEventListener('beforeunload', (event) => {
handler();
});
return () => {
handler();
document.removeEventListener('beforeunload', handler);
};
});
};
There might be a problem with using a listener inside of useEffect.
const useOnPageLeave = (handler) => {
useEffect(() => {
}, []); // useEffect will only get called once on component did mount
};
So You might want to consider trying to take your logic outside of useEffect
or
Adding a useState hook as a listener might be the way to go
const useOnPageLeave = (handler) => {
const [userLeaving, setUserLeaving] = useState(false);
useEffect(() => {
if (userLeaving) handler();
}, [userLeaving]); // useEffect will only get called once on component did mount AND each time userLeaving state updates
};
It looks like firefox and safari browsers are showing this message be default. Not much you can do about it. But there is a good news. It seems they are targeting only beforeunload event but not unload. So you could try to use that.
Here is more info about that: https://support.mozilla.org/en-US/questions/1304313
You can simply handle the unload event on window. For example the following code does an HTTP request while the page is being closed:
window.addEventListener('unload', function (event) {
(async () => {
const response = await fetch('/my-endpoint')
console.log(response)
})()
})
Do note however that the use of the unload event is discouraged. See MDN docs for details (TL;DR: it's unreliable, especially on mobile).

React state created with useState doesn't update automatically when my API changes

I have a problem when using the react useState states, the problem is that I want the state to update every time my api changes it's value, but the state just changes when the component is loaded, here is the code of the part that I'm having trouble with:
const [partida,setPartida] = useState();
const getNumero = ()=>{
axios.get(`https://carta-rusa.herokuapp.com/routes/find/${localStorage.getItem('partidaId')}`)
.then(res=>{
setPartida(res.data.partida.jugadores.length)
})
.catch(err=>console.log(err));
}
useEffect(()=>{getNumero();},[])
If the endpoint at /routes/find/ returns different data at different times, you have 2 main options:
Easiest to implement, but not very elegant: Polling. Make a request to that endpoint periodically, like every 5 minutes or every 30 seconds or something.
useEffect(() => {
const get = () => {
axios.get(`https://carta-rusa.herokuapp.com/routes/find/${localStorage.getItem('partidaId')}`)
.then(res => {
setPartida(res.data.partida.jugadores.length)
})
.catch(err => console.log(err));
};
get();
const intervalId = setInterval(get, 30000);
return () => clearInterval(intervalId);
}, []);
More elegant solution: Have the server send the updated value instead. When the component mounts, establish a WebSocket connection with the server, and have the server send new data whenever it changes. On the client, listen for socket messages, and when a message is seen, update state as needed.

How to cleanup an async funtion on componentDidUpdate using hooks

I've a component upon whose mounting, I'm subscribing to a socket(async task) and I've to unsubscribe(async) in upon unmounting. When the component updates, I've to unsubscribe to the old socket and subscribe to the new one. I'm not sure how to do this using react hooks. Attaching a sample codesandbox for reference.
https://codesandbox.io/s/clever-robinson-h0gq5
I'm not sure how you would like to implement the code within your sandbox but I think to call and async task within an effect you can do it like this:
useEffect(() => {
const timeout = async (msg, time) => {
await setTimeout(() => {
console.log(msg);
}, time);
}
timeout("subscribed to websocket", 2000);
return () => {
timeout("unsubscribed to websocket", 3000);
};
}, [match]);
To use an async await within useEffect you need to declare it within the function and call it from there.

Resources