Right way to update state with react socket hooks - reactjs

I am a beginner to react and I feel this question has been asked several times in the past but I can't see to understand what I am missing to get this working.
I am writing a simple browser based game. I am using socket-io to connect to the server and I am using a custom hook for obtaining state sent by the server
export const useSocketState = (serverUrl: string) => {
const [isConnected, setConnected] = useState(false)
const [socketId, setSocketId] = useState('')
const [gameState, setGameState] = useState(DEFAULT_INITIAL_STATE)
useEffect(() => {
const client = socket.connect(serverUrl)
client.on("connect", () => {
console.log("connected with id " + client.id)
setConnected(true)
setSocketId(client.id)
});
client.on("disconnect", () => {
setConnected(false)
});
client.on("gameStateUpdate", (data: ClientGameState) => {
console.dir("Recieved new state from server" + data);
console.dir("GameState is " + gameState);
setGameState(prevState => {
const newState = data
console.log(" new state is " + newState)
return newState
})
});
}, gameState);
return {gameState, isConnected, socketId}
}
I am using this hook in a component where I am referrring to the gameState returned by this hook.
I am expecting that when server sends a new state my gameState is actually set with a new state. I do see the log statement where a newState is printed when the server returns a new state but I am not seeing it being reflected in the component I am using.
Game component
import React from "react";
import {useSocketState} from "./useSocket";
import {ClientGameState} from "../game/GameState";
export function GameComponent(props: any) {
const {gameState, isConnected, socketId} = useSocketState("http://127.0.0.1:8080");
if (gameState.state === 'created') {
return (
<div>
<p> Game id for this game: {props.location.state.gameId} and playerId: {props.location.state.playerId} </p>
<p>{JSON.stringify(gameState)}</p>
</div>
);
} else {
return (
<div>
<p> Game state is not created </p>
</div>
)
}
}

export const useSocketState = (serverUrl: string) => {
const [isConnected, setConnected] = useState(false)
const [socketId, setSocketId] = useState('')
const [gameState, setGameState] = useState(DEFAULT_INITIAL_STATE);
function socketConnection(){
const client = socket.connect(serverUrl)
client.on("connect", () => {
console.log("connected with id " + client.id)
setConnected(true)
setSocketId(client.id)
});
client.on("disconnect", () => {
setConnected(false)
});
client.on("gameStateUpdate", (data: ClientGameState) => {
console.dir("Recieved new state from server" + data);
console.dir("GameState is " + gameState);
setGameState(prevState => {
const newState = data
console.log(" new state is " + newState)
return newState
})
});
}
useEffect(() => {
socketConnection();
}, []);
return {gameState, isConnected, socketId}
}
When you connect to the socket, you don't need to use useEffect() anymore.

Socket connection needed be made when component is mounted. So, you'll need second parameter of useEffect:
useEffect(() => {
// ... connection
}, []) // run on component mount
You may also use useEffect when some variable changes:
useEffect(() => { // additional useEffect hook
// ... changes being detected
}, [gameState])

How about just passing the data that is returned from the socket to your setGameState hook.
client.on("gameStateUpdate", (data: ClientGameState) => {
setGameState(data);
});

Related

Updating state based on changing API React

Is it possible to have an api constantly being called (in a loop until asked to stop) or have react automatically change state if the api changes?
I currently have a backend (/logs) that will send out logs in an array ex: [{log:"test"}, {log:"test1"}].
I have react able to pick that up and update state to display those logs on the front end when a button is clicked
axios.get("/logs").then(response =>{
let templog = response.data
let newLogs = [...logs]
for (let i = 0; i < templog.length; i++) {
if (templog[i].log !== "") {
newLogs.push({log: templog[i].log, id: uuidv4()})
}
}
setLogs(newLogs)
})
Right now, if I update the backend, I would have to reclick the button for the state to update rather than the state automatically updating based on the api
try setInterval in useEffect, also return clearInterval at end of useEffect
import React from 'react'
const ScheduleUpdate = (props) => {
const [logs, setLogs] = React.useState([])
const [run, setRun] = React.useState(true)
React.useEffect(() => {
const getLogs = () => {
fetch("/logs")
.then(r => r.json())
.then(d => { setLogs(d) })
}
if (run){
const handle = setInterval(getLogs, 1000);
return () => clearInterval(handle);
}
}, [run])
return (
<div>
<h1>ScheduleUpdate</h1>
{
logs.map(l => <div>{l}</div>)
}
<button onClick={() => setRun(false)}>Stop</button>
</div>
)
}
export default ScheduleUpdate

Get data from websocket and put it in state

I want to get data from https://finnhub.io/docs/api/websocket-trades.
console.log is working, but I can't get it rendered to the screen.
Here are the code for example:
// function to get websocket's data
const streamingStockPrice = (symbol) => {
const socket = new WebSocket('wss://ws.finnhub.io?token=c7d2eiqad3idhma6grrg');
// Connection opened -> Subscribe
socket.addEventListener("open", function (event) {
socket.send(JSON.stringify({ type: "subscribe", symbol }));
});
// Listen for messages
const stockPrice = socket.addEventListener("message", function (event) {
const str = event.data;
const currentStockPrice = str.substring(
str.indexOf(`"p"`) + 4,
str.indexOf(`,"s"`)
);
console.log(
"Message from server ",
str.substring(str.indexOf(`"p"`) + 4, str.indexOf(`,"s"`))
);
return currentStockPrice;
});
// Unsubscribe
var unsubscribe = function (symbol) {
socket.send(JSON.stringify({ type: "unsubscribe", symbol: symbol }));
};
return stockPrice;
};
export default streamingStockPrice;
// render to screen
import streamingStockPrice from "./streamingStockPrice";
const toScreen = () => {
const [stockPrice, setStockPrice] = useState("");
useEffect(() => {
setStockPrice(streamingStockPrice("GME"));
}, [stockPrice]);
return (
<div>
<h4>${stockPrice}</h4>
</div>
);
};
Please help. I appreciate it!
First I want to warn you, this bit of code will create an infinite loop. What you are saying here is, if the stockPrice changes than set the stockPrice.
const [stockPrice, setStockPrice] = useState(""); useEffect(() => { setStockPrice(streamingStockPrice("GME")); }, [stockPrice]);
Now that's out of the way, since you are using react you should create a component for displaying the stockPrice and pass the setStockPrice to the component. Like shown below:
const streamStockPrice = (symbol, setStockPrice) => { setStockPrice(currentStockPrice) }
This way you set the stockprice in your main component and can use/show it there.

useEffect function inside context unaware of state changes inside itself

I am building a messaging feature using socket.io and react context;
I created a context to hold the conversations that are initially loaded from the server as the user passes authentication.
export const ConversationsContext = createContext();
export const ConversationsContextProvider = ({ children }) => {
const { user } = useUser();
const [conversations, setConversations] = useState([]);
const { socket } = useContext(MessagesSocketContext);
useEffect(() => {
console.log(conversations);
}, [conversations]);
useEffect(() => {
if (!socket) return;
socket.on("userConversations", (uc) => {
let ucc = uc.map((c) => ({
...c,
participant: c.participants.filter((p) => p._id != user._id)[0],
}));
setConversations([...ucc]);
});
socket.on("receive-message", (message) => {
console.log([...conversations]);
console.log(message);
setConversations((convs) => {
let convIndex = convs.findIndex(
(c) => c._id === message.conversation._id
);
let conv = convs[convIndex];
convs.splice(convIndex, 1);
conv.messages.unshift(message);
return [conv, ...convs];
});
});
}, [socket]);
return (
<ConversationsContext.Provider
value={{
conversations,
setConversations,
}}
>
{children}
</ConversationsContext.Provider>
);
};
The conversations state is updated with the values that come from the server, and I have confirmed that on the first render, the values are indeed there.
Whenever i am geting a message, when the socket.on("receive-message", ...) function is called, the conversations state always return as []. When checking devTools if that is the case I see the values present, meaning the the socket.on is not updated with the conversations state.
I would appreciate any advice on this as I`m dealing with this for the past 3 days.
Thanks.
You can take "receive-message" function outside of the useEffect hook and use thr reference as so:
const onReceiveMessageRef = useRef();
onReceiveMessageRef.current = (message) => {
console.log([...conversations]);
console.log(message);
setConversations((convs) => {
let convIndex = convs.findIndex(
(c) => c._id === message.conversation._id
);
let conv = convs[convIndex];
convs.splice(convIndex, 1);
conv.messages.unshift(message);
return [conv, ...convs];
});
};
useEffect(() => {
if (!socket) return;
socket.on("userConversations", (uc) => {
let ucc = uc.map((c) => ({
...c,
participant: c.participants.filter((p) => p._id != user._id)[0],
}));
setConversations([...ucc]);
});
socket.on("receive-message", (...r) => onReceiveMessageRef.current(...r));
}, [socket]);
let me know if this solves your problem

React hooks with useState

I've got the following code:
export default function App() {
const [lastMessageId, setLastMessageId] = useState(0);
const [messages, setMessages] = useState([]);
const addMessage = (body, type) => {
const newMessage = {
id: lastMessageId + 1,
type: type,
body: body,
};
setLastMessageId(newMessage.id)
setMessages([...messages, newMessage]);
console.log("point 1", messages);
return newMessage.id;
}
// remove a message with id
const removeMessage = (id) => {
const filter = messages.filter(m => m.id !== id);
console.log("point 2", filter);
setMessages(filter);
}
// add a new message and then remove it after some seconds
const addMessageWithTimer = (body, type="is-primary", seconds=5) => {
const id = addMessage(body, type);
setTimeout(() => removeMessage(id), seconds*1000);
};
return (
...
);
}
I would like to know why after I setMessages at point 1, when I do console log it doesn't appear to be updated. This turns into a weird behaviour when I call addMessageWithTimer because when it calls removeMessage then it doesn't remove correctly the messages that I expect.
Could you please explain me how to do it?
Just like setState in class-components, the update functions of useState don't immediately update state, they schedule state to be updated.
When you call setMessages it causes react to schedule a new render of App which will execute the App function again, and useState will return the new value of messages.
And if you think about it from a pure JS perspective, messages can't change: it's just a local variable, (a const one, even). Calling a non-local function can't cause a local variable's value to change, JS just doesn't work that way.
#Retsam is correct in his explanation.
I think you would get an issue if you don't use setTimeout in addMessageWithTimer. Isn't it? But for now, it is correct.
If you don't want to give a timer of 5 seconds and still want to keep it running correctly, then give a timer of 0 seconds. It would still work okay.
what weird behavior your seeing?
when I tried your code, I'm able to remove the added message after 5 sec.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
let bodyText = "";
const [lastMessageId, setLastMessageId] = useState(0);
const [messages, setMessages] = useState([]);
const addMessage = (body, type) => {
if (body === "") return;
const newMessage = {
id: lastMessageId + 1,
type: type,
body: body
};
setLastMessageId(newMessage.id);
setMessages([...messages, newMessage]);
bodyText = "";
return newMessage.id;
};
// remove a message with id
const removeMessage = (id) => {
const filter = messages.filter((m) => m.id !== id);
console.log("point 2", filter);
setMessages(filter);
};
// add a new message and then remove it after some seconds
const addMessageWithTimer = (body, type = "is-primary", seconds = 5) => {
const id = addMessage(body, type);
setTimeout(() => removeMessage(id), seconds * 1000);
};
console.log("point 1", messages);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<input onChange={(e) => (bodyText = e.target.value)} />
<button onClick={(e) => addMessage(bodyText, "is-primary")}>
Add messsage
</button>
<button onClick={(e) => addMessageWithTimer(bodyText, "is-primary", 5)}>
Add temp messsage
</button>
{messages.map((message, id) => {
return (
<div key={id}>
<p>
{message.id} {message.body}
</p>
</div>
);
})}
</div>
);
}
#Retsam was very useful with his answer as I was able to understand the problem and find a proper solution.
here is the solution that I've found:
export default function App() {
const [lastMessageId, setLastMessageId] = useState(0);
const [messages, setMessages] = useState([]);
const addMessage = (body, type="is-primary") => {
const newMessage = {
id: lastMessageId + 1,
type: type,
body: body
};
setLastMessageId(newMessage.id)
setMessages([...messages, newMessage]);
return newMessage.id;
}
// delete messages after 5 seconds
useEffect(() => {
if (!messages.length) return;
const timer = setTimeout(() => {
const remainingMessages = [...messages];
remainingMessages.shift();
setMessages(remainingMessages);
}, 5*1000);
return () => clearTimeout(timer);
}, [messages]);
return (
...
);
}

Data coming in from socket doesn't update the state with useEffect

See demo here
I'm connecting to sockets (modelled by setTimeouts!) and getting an array. On mount I get an initial array. Then I keep listening for updates to the array. An update is sent as just the change and not a whole array.
I need to access the current state, but it's empty. Even though it looks fine in the render.
I think this might be a scoping or closure bug caused by numbers being empty at the time of calling addLater(), but I'm not sure what the solution is.
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [numbers, setNumbers] = useState([]);
useEffect(() => {
// initial connection to socket
setNumbers(["first", "second", "third"]);
// incoming messages from socket
addLater();
addMuchLater();
}, []);
const addLater = () => {
window.setTimeout(() => {
console.log("Why is the state empty? ", numbers);
const changedNumbers = [...numbers];
changedNumbers.splice(1, 1, "fourth");
setNumbers(changedNumbers);
}, 5000);
};
const addMuchLater = () => {
window.setTimeout(() => {
const changedNumbers = [...numbers];
changedNumbers.splice(2, 1, "fifth");
setNumbers(changedNumbers);
}, 10000);
};
return (
<div className="App">
{numbers.map((r, i) => (
<p>
{i}: {r}
</p>
))}
</div>
);
}
When the next value depends on the previous one it's best to write the code as a functional update so the code will always be acting on the latest value. As you ran into, your current code closes over the original value of numbers, which isn't what you want:
const addLater = () => {
window.setTimeout(() => {
setNumbers(prevNumbers => {
const changedNumbers = [...prevNumbers];
changedNumbers.splice(1, 1, "fourth");
return changedNumbers;
});
}, 5000);
};
const addMuchLater = () => {
window.setTimeout(() => {
setNumbers(prevNumbers => {
const changedNumbers = [...prevNumbers];
changedNumbers.splice(2, 1, "fifth");
return changedNumbers;
});
}, 10000);
};
setState is asynchronous so the calls to state that happen in the functions later on get the state as it was when the component rendered, not as it is after the new state was set. You can use a callback function as the second argument:
useEffect(() => {
// initial connection to socket
setNumbers(["first", "second", "third"],
()=>{
// incoming messages from socket
addLater();
addMuchLater();
}), []);
See:
https://upmostly.com/tutorials/how-to-use-the-setstate-callback-in-react

Resources