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
I know similar questions are bouncing around the web for quite some time but I still struggle to find a decision for my case.
Now I use functional React with hooks. What I need in this case is to set a state and AFTER the state was set THEN to start the next block of code, maybe like React with classes works:
this.setState({
someStateFlag: true
}, () => { // then:
this.someMethod(); // start this method AFTER someStateFlag was updated
});
Here I have created a playground sandbox that demonstrates the issue:
https://codesandbox.io/s/alertdialog-demo-material-ui-forked-6zss6q?file=/demo.tsx
Please push the button to get the confirmation dialog opened. Then confirm with "YES!" and notice the lag. This lag occurs because the loading data method starts before the close dialog flag in state was updated.
const fireTask = () => {
setOpen(false); // async
setResult(fetchHugeData()); // starts immediately
};
What I need to achieve is maybe something like using a promise:
const fireTask = () => {
setOpen(false).then(() => {
setResult(fetchHugeData());
});
};
Because the order in my case is important. I need to have dialog closed first (to avoid the lag) and then get the method fired.
And by the way, what would be your approach to implement a loading effect with MUI Backdrop and CircularProgress in this app?
The this.setState callback alternative for React hooks is basically the useEffect hook.
It is a "built-in" React hook which accepts a callback as it's first parameter and then runs it every time the value of any of it's dependencies changes.
The second argument for the hook is the array of dependencies.
Example:
import { useEffect } from 'react';
const fireTask = () => {
setOpen(false);
};
useEffect(() => {
if (open) {
return;
}
setResult(fetchHugeData());
}, [open]);
In other words, setResult would run every time the value of open changes,
and only after it has finished changing and a render has occurred.
We use a simple if statement to allow our code to run only when open is false.
Check the documentation for more info.
Here is how I managed to resolve the problem with additional dependency in state:
https://codesandbox.io/s/alertdialog-demo-material-ui-forked-gevciu?file=/demo.tsx
Thanks to all that helped.
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])```
Maybe I'm missing something, but I can't find a solution for that.
I have a React component and want to run some periodic background job (setInterval)
This job use some information of the components state and should also be able to change that.
My component:
export default function Form() {
const [state, setState] = useState<IState>(initialState);
useEffect(() => {
//init component and other stuff
// trigger background Task
setInterval(backgroundJob, 10000);
}, []);
function backgroundJob(){
state.xxx
}
function handleButtonClick(){
state.xxx = state.xxx + 1;
setState({
...state,
});
}
}
The main problem is, the backgroundJob Function is able to access the state, but it's only the initalState. If the state is updated, (e.g. on a button click via the handleButtonClick() function), the next Interval tick still has old values in the state
Do I misunderstood some basic concept here?
You need to add backgroundJob to you dependency list, but also you need to clear this interval when the effect is re-run:
Note that in the way your code is written this will happen every time the component renders. You can use useCallback to optimize if needed.
useEffect(() => {
//init component and other stuff
// trigger background Task
const intervalId = setInterval(backgroundJob, 10000);
return () => clearInterval(intervalId)
}, [backgroundJob]);
In not doing so, the backgroundJob you are running is the one from the first render which only "see"s (via its closure) the initial state (AKA Stale Closure)
I have a simple react component that sends and receives messages via SocketIO. My state hooks look like this
const [newMessage, createMessage] = useState('');
const [messages, setMessage] = useState([]);
const [isConnected, setConnection] = useState(false);
The idea is that the user can compose and send the message through the socket and then will receive the message back. The user inputs some text into the text area. The contents of the message are saved to the "newMessage" local state variable - implementation details below if necessary - and then the user sends the newMessage through the socket.
const sendMessage = () => {
const currentMessage = {...newMessage}; //reduced for the sake of brevity
socket.emit('message', currentMessage);
createMessage('');
};
const composeMessage = (
<>
<div>
<textarea rows="2" cols="28" placeholder="Chat Message" onKeyDown={(e) => handleKeyDown(e)} onChange={(e) => createMessage(e.currentTarget.value)} value={newMessage} />
</div>
<div>
<button onClick={sendMessage}><i className="ra ra-horn-call" /></button>
</div>
</>
);
Serverside, the message is received and immediately transmitted back to the room. I put the function that listens for the message response into the useEffect hook. The idea was to use the hook in place of componentDidMount, so I originally implemented it by passing an empty array as the second argument to useEffect. When I did this, any time a message was received, it would appear to clear out my "messages" state and then replace the contents with the new message. From the browser, it looked like there was only ever one message that could be held in state at a time - the previous message kept getting replaced. I tired passing the "messages" state in the second argument array and the message now appeared to get appended to the state and the component re-rendered as it was received -cool right, thats what I wanted?
I noticed that I was having performance issues on every subsequent message and ultimately discovered that the socket listener was getting re-applied each render. In order to stop this a colleague suggested that I add a boolean to state that should prevent the socket from getting re-added whenever there is a re-render. I updated the component and the result was the same as it was the first time when I was not passing the messages state into the second argument - it kept replacing the first message in state and did not append new messages. I just want the listener to update the local state and rerender the messages any time a new message comes through the socket. I am kind of at a loss for how to do this with hooks. Anyone have any ideas?
The final iteration of the useEffect hook that I wrote is below.
useEffect(() => {
debugger
if (!isConnected) {
socket.on('message', (message) => {
const nextState = messages.slice();
nextState.push(message);
setMessage(nextState);
debugger
});
setConnection(true);
}
}, [messages, setConnection]);
From your code, i think useEffect will run and make multiple subscriptions to listen on every message, on each render, whenever there's a change to message and connectionState. This is because a react component will render whenever there's a state change. As websocket connections and listeners can be considered side effects, you have to implement a return callback to handle them when your component unmounts.
Assuming all your websocket handling and chat message logic are within a single react component, you probably only need to create the web socket connection and listener once on mount like so, and provide a return call back to handle the unmounts, for scenarios like when you do a page refresh, or your component relies on a parent state which might trigger a re-render:
useEffect(() => {
socket.connect();
socket.on('connect', () => {
console.log('socket connected:', socket.connected);
});
socket.on('message',...);
}
return () => {
console.log('websocket unmounting!!!!!');
socket.off();
socket.disconnect();
};
}, []);