React/Socket.io not displaying latest message passed down as prop - reactjs

I am working on a chat application using React and socket.io. Back end is express/node. The relevant components are:
Room.js --> Chat.js --> Messages.js --> Message.js
messageData received from the server is stored in state in Room.js. It is then passed down through Chat.js to Messages.js, where it is mapped onto a series of Message.js components.
When messages are received, they ARE appearing, but only after I start typing in the form again, triggering messageChangeHandler(). Any ideas why the Messages won't re-render when a new message is received and added to state in Room.js? I have confirmed that the state and props are updating everywhere they should be--they just aren't appearing/re-rendering until messageChangeHandler() triggers its own re-render.
Here are the components.
Room.js
export default function Room(props) {
const [messagesData, setMessagesData] = useState([])
useEffect(() => {
console.log('the use effect ')
socket.on('broadcast', data => {
console.log(messagesData)
let previousData = messagesData
previousData.push(data)
// buildMessages(previousData)
setMessagesData(previousData)
})
}, [socket])
console.log('this is messagesData in queue.js', messagesData)
return(
// queue counter will go up here
// <QueueDisplay />
// chat goes here
<Chat
profile={props.profile}
messagesData={messagesData}
/>
)
}
Chat.js
export default function Chat(props) {
// state
const [newPayload, setNewPayload] = useState({
message: '',
sender: props.profile.name
})
// const [messagesData, setMessagesData] = useState([])
const [updateToggle, setUpdateToggle] = useState(true)
const messageChangeHandler = (e) => {
setNewPayload({... newPayload, [e.target.name]: e.target.value})
}
const messageSend = (e) => {
e.preventDefault()
if (newPayload.message) {
socket.emit('chat message', newPayload)
setNewPayload({
message: '',
sender: props.profile.name
})
}
}
return(
<div id='chatbox'>
<div id='messages'>
<Messages messagesData={props.messagesData} />
</div>
<form onSubmit={messageSend}>
<input
type="text"
name="message"
id="message"
placeholder="Start a new message"
onChange={messageChangeHandler}
value={newPayload.message}
autoComplete='off'
/>
<input type="submit" value="Send" />
</form>
</div>
)
}
Messages.js
export default function Messages(props) {
return(
<>
{props.messagesData.map((data, i) => {
return <Message key={i} sender={data.sender} message={data.message} />
})}
</>
)
}
Message.js
export default function Message(props) {
return(
<div key={props.key}>
<p>{props.sender}</p>
<p>{props.message}</p>
</div>
)
}
Thank you in advance for any help!

I don't think that your useEffect() function does what you think it does.
Red flag
Your brain should generate an immediate red flag if you see a useEffect() function that uses variables declared in the enclosing scope (in a closure), but those variables are not listed in useEffect()'s dependencies (the [] at the end of the useEffect())
What's actually happening
In this case, messagesData in being used inside useEffect() but not declared as a dependency. What happens is that after the first broadcast is received and setMessagesData is called, messagesData is no longer valid inside useEffect(). It refers to an array, from the closure when it was last run, which isn't assigned to messageData any longer. When you call setMessagesData, React knows that the value has been updated, and re-renders. It runs the useState() line and gets a new messagesData. useEffect(), which is a memoized function, does NOT get recreated, so it's still using messagesData from a previous run.
How to fix it
Clean up useEffect()
Before we start, let's eliminate some of the noise in the function:
useEffect(() => {
socket.on('broadcast', data => {
setMessagesData([...messagesData, data])
})
}, [socket])
This is functionally equivalent to your code, minus the console.log() messages and the extra variable.
Let's go one step further and turn the handler into a one-liner:
useEffect(() => {
socket.on('broadcast', data => setMessagesData([...messagesData, data]));
}, [socket])
Add missing dependencies
Now, let's add the missing dependencies!
useEffect(() => {
socket.on('broadcast', data => setMessagesData([...messagesData, data]));
}, [socket, messagesData])
Technically, we also depend on setMessagesData(), but React has this to say about setState() functions:
React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
Too many cooks
The useEffect() function is looking good, but we still depend on messagesData. This is a problem, because every time socket receives a broadcast, messagesData changes, so useEffect() is re-run. Every time it is re-run, it adds a new handler/listener for broadcast messages, which means that when the next message is received, every handler/listener calls setMessagesData(). The code might still accidentally work, at least logic-wise, because listeners are usually called, synchronously, in the order in which they were registered, and I believe that if multiple setState() calls are made during the same render, React only re-renders once using the final setState() call. But it will definitely be a memory leak, since we have no way to unregister all of those listeners.
This tiny problem would normally end up being a huge pain to solve, because to fix this problem, we would need to unregister the old listener every time we registered a new one. And to unregister a listener, we call removeListener() function with the same function we registered - but we don't have that function anymore. Which means we need to save the old function as state or memoize it, but now we also have another dependency for our useEffect() function. Avoiding a continuous loop of infinite re-renders turns out to be non-trivial.
The trick
It turns out that we don't have to jump through all of those hoops. If we look closely at our useEffect() function, we can see that we don't actually use messagesData, except to set the new value. We're taking the old value and appending to it.
The React devs knew that this was a common scenario, so there's actually a built-in helper for this. setState() can accept a function, which will immediately be called with the previous value as an argument. The result of this function will be the new state. It sounds more complicated than it is, but it looks like this:
setState(previous => previous + 1);
or in our specific case:
setMessagesData(oldMessagesData => [...oldMessagesData, data]);
Now we no longer have a dependency on messagesData:
useEffect(() => {
socket.on('broadcast', data => setMessagesData(oldMessagesData => [...oldMessagesData, data]);
}, [socket])
Being polite
Remember earlier when we talked about memory leaks? It turns out this can still happen with our latest code. This Component may get mounted and unmounted multiple times (for example, in a Single-Page App when the user switches pages). Each time this happens, a new listener is registered. The polite thing to do is to have useEffect() return a functions which will clean up. In our case this means unregistering/removing the listener.
First, save the listener before registering it, then return a function to remove it
useEffect(() => {
const listener = data => setMessagesData(oldMessagesData => [...oldMessagesData, data];
socket.on('broadcast', listener);
return () => socket.removeListener('broadcast', listener);
}, [socket])
Note that our listener will still be dangling if socket changes, and since it's not clear in the code where socket comes from, whatever changes that will also have to remove all old listeners, e.g. socket.removeAllListeners() or socket.removeAllListeners('broadcast').

Changing the useEffect in room to contain the following fixed the issue:
useEffect(() => {
console.log('the use effect ')
socket.on('broadcast', data => {
console.log(messagesData)
// let previousData = messagesData
// previousData.push(data)
// setMessagesData(previousData)
setMessagesData(prev => prev.concat([data]))
})
}, [socket])```

Related

RXJS subscribe callback doesn't have access to current react state (functional component)

I am using React with functional components in combination with useState() and RxJs.
I'm subscribing to a BehaviorSubject in my useEffect[] and everytime a new message is published, I want to check the current state of my component to decide which steps to take.
But: Even though in my program flow I can clearly see that my state has a certain value, the subscribe callback always only shows the initial empty value. When I stop execution in the middle of the callback, I can see that the "outdated" state is in the closure of the callback.
Why is this?
I've broken it down to those essential code parts:
function DesignView() {
const [name, setName] = useState("");
useEffect(() => {
console.log(name); // <--- This always shows correctly, of course
}, [name]);
useEffect(() => {
// even if this is the ONLY place I use setName() ... it doesn't work
setName("Test Test Test Test");
let subscription = directionService.getDirection().subscribe(() => {
console.log(name); // <--- this only ever shows "" and never "Test Test Test Test"
// no matter at what point of time the published messages arrive!
});
return () => {
subscription.unsubscribe();
}
}, []);
return (
...
);
}
The cause of this problem is that a non-react callback only ever sees a static copy of the state. The same problem appears in the useEffect cleanup function.
Solution:
Either
Add a ref to the state variable. Change the ref.current whenever the state changes and use the ref in the callback
Add the state variable to the dependency array of useEffect and unsubscribe/subscribe every time

Why isn't my state hook updating correctly?

I've created a minimal cutting of my code to show the issue, seen below.
const PlayArea = (props) => {
const [itemsInPlay, setItemsInPlay] = useState([
{id: 'a'},
{id: 'b'}
]);
const onItemDrop = (droppedItem) => {
setItemsInPlay([...itemsInPlay, droppedItem]);
};
return (
<>
<Dropzone onDrop={onItemDrop} />
<div>
{itemsInPlay.map(item => (
<span
key={item.id}
/>
))}
</div>
</>
);
};
The dropzone detects a drop event and calls onItemDrop. However, for reasons I don't understand, I can only drop in one item. The first item I drop is correctly appended to itemsInPlay and it re-renders correctly with a third span in addition to the starting two.
However, any subsequent item I drop replaces the third item rather than being appended. It's as though onItemDrop had a stored reference to itemsInPlay which was frozen with the initial value. Why would that be? It should be getting updated on re-render with the new value, no?
The Dropzone sets its subscription token only once, when the component is initially rendered. When that occurs, the callback passed to setSubscriptionToken contains a stale value of the onCardDrop prop - it will not automatically update when the component re-renders, since the subscription was added only once.
You could either unsubscribe and resubscribe every time onCardDrop changes, using useEffect, or use the callback form of setItemsInPlay instead:
const onItemDrop = (droppedItem) => {
setItemsInPlay(items => [...items, droppedItem]);
};
This way, even if an old version of onItemDrop gets passed around, the function won't depend on the current binding of itemsInPlay being in the closure.
Another way to solve it would be to change Dropzone so that it subscribes not just once, but every time the onCardDrop changes (and unsubscribing at the end of a render), with useEffect and a dependency array.
Regardless of what you do, it would also be a good idea to unsubscribe from subscriptions when the PlayArea component dismounts, something like:
const [subscriptionToken, setSubscriptionToken] = useState<string | null>(null);
useEffect(
() => {
const callback = (topic: string, dropData: DropEventData) => {
if (wasEventInsideRect(dropData.mouseUpEvent, dropZoneRef.current)) {
onCardDrop(dropData.card);
setDroppedCard(dropData.card);
}
};
setSubscriptionToken(PubSub.subscribe('CARD_DROP', callback));
return () => {
// Here, unsubscribe from the CARD_DROP somehow,
// perhaps using `callback` or the subscription token
};
},
[] // run main function once, on mount. run returned function on unmount.
);

useEffect when onClick

I'm getting stuck about how to use effects together with app logic.
Suppose this component:
import React, { useEffect, useState } from "react";
export default function App() {
const [query, setQuery] = useState('');
useEffect( () => {
fetch('https://www.google.com?q='+query)
.then(response => console.log(response))
}); // depends on what?
return (
<div>
<input onChange={e => setQuery(e.target.value)} value={query} />
<button>Ask Google about {query}</button>
</div>
);
}
I want that:
when (and only) the user clicks the button the fetch is run with the correct query value of the input
if the fetch is still in progress and the user clicks, the fetch is skipped but the effect is fired (meaning: I intentionally not disable the button, I want that the effect function is run, but I put a check inside that function not to execute the fetch).
Problems:
The effect shouldn't fire on mount (it wouldn't make any sense)
The effect shouldn't fire when the query changes, but if I don't put the query variable inside the useEffect dependency array, React complains (react-hooks/exhaustive-deps)
The effect should fire when the user click on the button; I achieved this for example using a fake state isRun, setting onClick={setIsRun(true)}, making the effect depending on [isRun], setting setIsRun(false) at the end of the effect function, and checking if (!isRun) at the beginning of the effect function to prevent that when is set to false from the effect itself it is run again since the state changes. This works, but I find it very verbose and uncomfortable...
The effect should fire if the button is clicked again (with the same query value or not) and the previous fetch has not yet finished without running the fetch: with the previous solution with isRun it wouldn't fire because isRun is already set to 1 so there is no state change; maybe with another state there is a way, but again very verbose and counterintuitive.
Most importantly: the code should be clean and readable without using "tricks"!
How would you write such a component?
It sounds like you shouldn't be using useEffect for this at all. You want this to happen on a user action, not as an effect:
when (and only) the user clicks the button the fetch is run with the correct query value of the input
Remove useEffect and create a function to handle the click:
const handleClick = (e) => {
fetch('https://www.google.com?q='+e.target.value)
.then(response => console.log(response));
};
And pass that function to the component:
<button onClick={handleClick}>Ask Google about {query}</button>
What seems confusing here are these requirements:
if the fetch is still in progress and the user clicks, the fetch is skipped but the effect is fired
The effect should fire if the button is clicked again (with the same query value or not) and the previous fetch has not yet finished without running the fetch
The only thing the function does is execute a fetch. So should that operation happen or not? Your proposed solution of keeping state in a variable (isRun) to determine if it should happen or not should work in this case. I think the problem before was mixing that up with useEffect when all you really want is a function. Add isRun to state and update it accordingly when performing the operation:
const [isRun, setIsRun] = useState(false);
const handleClick = (e) => {
if (isRun) { return; }
setIsRun(true);
fetch('https://www.google.com?q='+e.target.value)
.then(response => {
console.log(response);
setIsRun(false);
});
};
I'm not sure if you want to insist on using useEffect but it does not seem appropriate for this situation. What I could do is call a function on button click.
import React, { useEffect, useState } from "react";
export default function App() {
const [query, setQuery] = useState('');
const handleQuery = (query) => {
fetch('https://www.google.com?q='+query)
.then(response => console.log(response))
}
return (
<div>
<input onChange={e => setQuery(e.target.value)} value={query} />
<button onClick={() => handleQuery(query)}>Ask Google about {query}</button>
</div>
);
}

React state resets with Socket.io

I am currently struggling with issues pertaining to socket.io and React. Whenever socket.on() is called, the state resets, and all the previous chat data is gone. In other words, whenever the user receives a message, the 'messages' state is reset.
I saw a similar post on this Socket.io resets React state?, but I couldnt seem to apply the same solution to my issue. Any help would be appreciated!
function ChatComponent(props) {
const [messages, setMessages] = useState([]);
const [socket, setSocket] = useState(socketioclient("*********"));
function socket_joinRoom(room) {}
function _onMessageUpdate(message) {
setMessages([
...messages,
{
author: "them",
type: "text",
data: { text: message },
},
]);
}
useEffect(() => {
socket_joinRoom(parseInt(props.props[0], 10));
socket.on("updateMessage", (message) => {
//** When this is called, the state resets*
console.log(messages);
_onMessageUpdate(message);
});
return () => {
socket.off("updateMessage");
socket.disconnect();
};
}, []);
function _onMessageWasSent(message) {
setMessages([...messages, message]);
socket.emit("sendMessage", message.data.text);
}
return (
<div className="chatComponent" style={{ height: "100%" }}>
<Launcher
agentProfile={{
teamName: `Ongoing: Room #${props.props[0]}`,
}}
onMessageWasSent={_onMessageWasSent}
messageList={messages}
isOpen={true}
showEmoji
/>
</div>
);
}
first of all you need to separate the joining room logic in it's own useEffect
useEffect(()=>{
socket_joinRoom(parseInt(props.props[0],10));
},[props.props[0]])
cause you are going to listen to the changes of the received messages in another useEffect
so you don't need to init the joining logic every time you receive anew message
and for the message function it can be another useEffect which listen to the received "messages" as this following code
useEffect(()=>{
socket.on('message',(message)=>{
setMessages((currentMessages)=>[...currentMessages,message])
}
},[])
this useEffect will fire and listen to the changes and add the new message to the previous messages
I'm answering this for other people searching along the terms of " why does socket.io reset my state when .on or other event listeners are used.
This turned out to be a simple useEffect() behavior issue and not an issue within socket.io. Although this is the case I feel that socket.io made an oversight not communicating this in their react hooks documentation.
Below is problem code.
useEffect(() => {
socket_joinRoom(parseInt(props.props[0], 10));
socket.on("updateMessage", (message) => {
//** When this is called, the state resets*
console.log(messages);
_onMessageUpdate(message);
});
return () => {
socket.off("updateMessage");
socket.disconnect();
};
}, []);
The above useEffect runs a single time at the mounting of this component. One may deduce that the variables used in writing this useEffect would remain dynamic and run once executed by socket.io, however it is not dynamic, it is static and the useState called in 'update_message' remains static and at the value it was during the mounting of the parent component.
To reiterate your component mounts and because of [] at the the end of useEffect the useEffect runs a single time at component mount. There is abstraction within socket.io in which the callback function ran by .on('update_message') is saved and the variable values are frozen at the time of mount.
This obviously seems like your state is being reset however that is not the case. You are just using the out dated states.
Like the other poster responded if you have a socket.io listener that needs to change dynamically based on variables or states that update you need to write a separate use effect with a dependency based on the state you wish to remain dynamic, you MUST also return a socket.off('updateMessage') so that the old listener is removed with the old state, everytime your state is updated. If you don't do this, you may also get old out of date states
useEffect(()=>{
socket.on('updateMessage', (message)=>{
onMessageUpdate(message)
})
return ()=>{
socket.off('updateMessage')
},[messages])

useEffect has missind dependencies when I want to run it always just once on mount

I have a component that's supposed to read a property from the component (which is either string "fill" or string "stroke") and pull the according key from an object to read it's context.
This gets mounted as soon as an object is selected, accesses the active object and pulls out it's color either as a fill color or a stroke color.
useEffect(() => {
setButtonColor(context.objects.getActiveObject()[props.type]);
}, []); //on mount
Mounting it like this:
<ColorPicker type="fill" />
<ColorPicker type="stroke" />
This supposed to run only once on mount. I thought when the dep array is empty, it runs on any case once when it's mounted.
So how do I run something once on mount utilizing props and context?
And why does it need a dependency at all when I want it to ALWAYS run only ONCE on mount, no matter what?
It's best to move away from the thinking that effects run at certain points in the lifecycle of a component. While that is true, a model that might help you better get to grips with hooks is that the dependency array is a list of things that the effect synchronizes with: That is, the effect should be run each time those things change.
When you get a linter error indicating your dependency array is missing props, what the linter is trying to tell you is that your effect (or callback, or memoization function) rely on values that are not stable. It does this because more often than not, this is a mistake. Consider the following:
function C({ onSignedOut }) {
const onSubmit = React.useCallback(() => {
const response = await fetch('/api/session', { method: 'DELETE' })
if (response.ok) {
onSignedOut()
}
}, [])
return <form onSubmit={onSubmit}>
<button type="submit">Sign Out</button>
</form>
}
The linter will issue a warning for the dependency array in onSubmit because onSubmit depends on the value of onSignedOut. If you were to leave this code as-is, then onSubmit will only be created once with the first value of onSignedOut. If the onSignedOut prop changes, onSubmit won't reflect this change, and you'll end up with a stale reference to onSignedOut. This is best demonstrated here:
import { render } from "#testing-library/react"
it("should respond to onSignedOut changes correctly", () => {
const onSignedOut1 = () => console.log("Hello, 1!")
const onSignedOut2 = () => console.log("Hello, 2!")
const { getByText, rerender } = render(<C onSignedOut={onSignedOut1} />)
getByText("Sign Out").click()
// stdout: Hello, 1!
rerender(<C onSignedOut={onSignedOut2} />)
getByText("Sign Out").click()
// stdout: Hello, 1!
})
The console.log() statement does not update. For this specific example that would probably violate your expectations as a consumer of the component.
Let's take a look at your code now.
As you can see, this warning is essentially stating that your code might not be doing what you think it is doing. The easiest way to dismiss the warning if you're sure you know what you're doing is to disable the warning for that specific line.
useEffect(() => {
setButtonColor(context.objects.getActiveObject()[props.type]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); //on mount
The correct way to do this would be to place your dependencies inside of the array.
const { type } = props
useEffect(() => {
setButtonColor(context.objects.getActiveObject()[type]);
}, [context, type]);
This would, however, change the button colour every time type changed. There's something to note here: You're setting state in response to props changing. That's called derived state.
You only want that state to be set on the initial mount. Since you only want to set this on the initial mount, you could simply pass your value to React.useState(initialState), which would accomplish exactly what you want:
function C({ type }) {
const initialColor = context.objects.getActiveObject()[type];
const [color, setButtonColor] = React.useState(initialColor);
...
}
This still leaves the problem that the consumer might be confused as to why the view does update when you change the props. The convention that was common before functional components took off (and one I still use) is to prefix props that are not monitored for changes with the word initial:
function C({ initialType }) {
const initialColor = context.objects.getActiveObject()[initialType];
const [color, setButtonColor] = React.useState(initialColor);
}
You should still be careful here, though: It does mean that, for the lifetime of C, it will only ever read from context or initialType once. What if the value of the context changes? You might end up with stale data inside of <C />. That might be acceptable to you, but it's worth calling out.
React.useRef() is indeed a good solution to stabilize values by only capturing the initial version of it, but it's not necessary for this use-case.
This is my workaround for the issue:
Set the color to a variable and then use that variable to set the button color on mount of the component.
const oldColor = useRef(context.objects.getActiveObject()[props.type]);
useEffect(() => {
setButtonColor(oldColor.current);
}, []); //on mount
useRef returns a mutable ref object whose .current property is
initialized to the passed argument (initialValue). The returned object
will persist for the full lifetime of the component.

Resources