React Hooks: Function still has old state? - reactjs

I recently added asynchronous code (Sockets) to a project and every time the state is called the original state value is passed. So far I've tried passing a callback to setState, and although that seemed to help, I ran into another issue the state no longer updated, i.e. the child component stopped re-rendering.
Below is the code which is most relevant. If you want another piece of code, don't hesitate to ask.
This code is called to update the state, which is a series of nested dictionaries, one named card contains an array of objects. The code updates the card we are calling it from, calls a function which returns a modified copy of the object and then passed that to a callback. Previously this was done by passing a copy of the original state, but that has the same issue.
const updateItem = useCallback(
(id, field, additionalData, value) => {
const { card, index } = findCard(id);
console.log("Updating item " + id);
const updatedCard = updateObject(card, additionalData, field, value);
setData((prevState) => {
const newState = {...prevState};
newState["HIT"]["data"][index] = updatedCard;
return newState;
});
},
[data]
);
The update function is called from an input in a child component. The state updates, but next time it is called the userInput value is "".
<input
type="text"
id="message"
autoComplete="on"
placeholder="Type a message"
value={props.Item['userInput']}
onChange={e => {props.updateItem(props.Item["id"],
"userInput",
{},
e.target.value);
}
}
/>
Any help would be greatly appreciated!

Related

state data not changing in the render/display

I want to change the property amount in a state object using buttons (increment and decrement). I checked using console.log and the property's value is changing when the buttons are clicked, but the displayed number is not changing. why is that? what am I doing wrong?
here's my code: (codesandbox)
import React, { useState, useEffect } from "react";
import { Button } from "react-bootstrap";
export default function App() {
const [data, setData] = useState({});
useEffect(() => {
const temp = {
id: 1,
name: "apple",
amount: 10
};
setData(temp);
}, []);
const handleInc = () => {
let temp = data;
temp.amount++;
console.log("increment", temp.amount);
setData(temp);
};
const handleDec = () => {
let temp = data;
temp.amount--;
console.log("decrement", temp.amount);
setData(temp);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<label>name: {data.name}</label>
<br />
<label>amount: {data.amount}</label>
<br />
<Button onClick={handleDec}>Reduce</Button>
<Button onClick={handleInc}>Increase</Button>
</div>
);
}
let temp = data;
temp.amount++;
setData(temp);
does not update data, as temp == data even after temp.amount++.
The state setter accepts either a new object or a state update callback.
Since you are updating state using it's old value, you need a state update callback,
that returns a new object (via cloning).
setData((data)=> {
let temp = {...data}; // or Object.assign({}, data);
temp.amount++;
return temp;
}
Likewise, for decrementing.
See https://beta.reactjs.org/learn/updating-objects-in-state
and https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
You have 2 issues here. First one is that you using object as your state and object is reference type. When you do let temp = data you just referecing the same exact "data" object using different variable (pointer) "temp". Simply speaking once you change property in one variable, it "changes" the other. Now that also means that whatever you do, temp will always be equal to data cause they are referencing the same object. So to React state it means that it never really changes in your case, when you do setState you passing the same exact reference, so React sees that nothing changed - so it doesn't trigger re-render. Hope it is clear.
Fix in this case is to create a copy of object, in your case it could be simply setState({...temp})
The second issue in your case is that you are not using functional setState, which in your case is needed. The way you wrote it, might lead to bugs and unexpected behaviours, basically whenever you need to modify the state based on previous state value - you need to use functional setState. There are a lot of topics on this, let me reference just one - https://www.freecodecamp.org/news/functional-setstate-is-the-future-of-react-374f30401b6b/
In your case correct solution would be setState((prevState) => ({...prevState, amount: prevState.amount + 1}))
I think you should use Callback with useState to resolve this bug.
const handleInc = () => {
setData((prevState) => ({ ...prevState, amount: prevState.amount + 1 }));
};
const handleDec = () => {
setData((prevState) => ({ ...prevState, amount: prevState.amount - 1 }));
};
Take note of both of the other answers by Nice Books and Nikita Chayka they both touch on important topics that will help you avoid this issue in the future. If you want to update an object using states you need to reconstruct the entire object when you reset the object. I made a fork of your sandbox you can take a look at of a working example that should solve your issue Forked sandbox.
Also the docs reference this issue as well Doc reference.
Please let me know if you need any additional information.

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

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])```

Updating one State with two fields?

I am trying to update my state data based on the users input in two fields and I'm not sure if Im going about it the right way.
The parent component Encounter.js holds the state I will try and limit the amount of code I add here so my issue is clear. So in ComponentDidUpdate I set the state with an object and create an update function to update the state. I pass the two values inside my state to another component PatientInfo along with the update state function:
componentDidUpdate(prevProps) {
if (this.props.details && this.props.details.medicalIntake && !prevProps.details.medicalIntake) {
this.setState({ pertinentMedications: {
covid19Protocol: this.props.details.medicalIntake.pertinentMedications.covid19Protocol,
note: "" || this.props.details.medicalIntake.pertinentMedications.notes
}})
}
}
pertinentMedicationsChange = (newValues) => {
this.props.setIdleTime();
this.props.setState({pertinentMedications: newValues});
}
return (
<PatientInfo
covid19Protocol={this.state.pertinentMedications.covid19Protocol}
pertinentMedicationsNote={this.state.pertinentMedications.note}
pertinentMedicationsChange={this.pertinentMedicationsChange}
/>
)
PatientInfo.js simply passes the props down.
<PertinentMedications
covid19Protocol={this.props.covid19Protocol}
pertinentMedicationsNote={this.props.pertinentMdicationsNote}
pertinentMedicationsChange={this.props.pertinentMedicationsChange}
/>
PertinentMedications.js is where the user input will be collected:
const PertinentMedications = ({
covid19Protocol,
pertinentMedicationsNote,
pertinentMedicationsChange
}) => {
const [isChecked, setIsChecked] = useState(covid19Protocol)
const onClick = (field, value) => {
setIsChecked(!isChecked)
pertinentMedicationsChange( {[field]: value})
}
const onNoteChange = (field, value) => {
pertinentMedicationsChange( {[field]: value})
}
return(
<ContentBlock title="Pertinent Medications and Supplements">
<CheckToggle onChange={() => onClick("covid19Protocol", !covid19Protocol)} checked={isChecked}>
<p>Patient has been receiving the standard supportive care and supplements as per COVID-19 protocol.</p>
</CheckToggle>
<Input
type="textarea"
name="pertinentMedications"
onChange={e => onNoteChange("notes" ,e.target.value)}
value={pertinentMedicationsNote}
/>
</ContentBlock>
)
}
export default PertinentMedications;
My true question lies within the pertinentMedicationsChange function as Im not sure how to take the data im getting from the PertinentMedications component and format it to be placed in the state. First Im not sure if I can update the state the way im trying to with these two independent fields that send their data to this function to change the state? And If it is possible Im not sure how to properly setup the key value pairs when i call setState. Can anyone help?
it seems that you are calling this.props.setState instead of this.setState. Second, this.setState also accepts a function which first param is the previous state. In this way you can use it to prevent its key values saved from pertinentMedications to be overwritten. fwiw, it's better to be consistent, not mixing hooks with react component based.
pertinentMedicationsChange = (newValues) => {
this.props.setIdleTime();
this.setState((state) => ({
// you create a new object with previous values, while newValues updates the proper keys, but not removing other keys
pertinentMedications: { ...state.pertinentMedications,...newValues}
});
)};

setInterval with updated data in React+Redux

I have setInterval setup to be working properly inside componentDidMount but the parameters are not updated. For example, the text parameter is the same value as when the component initially mounted, despite being changed in the UI. I've confirmed text's value is correctly updated in Redux store but not being passed to this.retrieveData(text). I suspect the const { text } = this.props set the value in componentDidMount, which forbids it from updating despite it being different. How would I go about this issue?
Code below is an example, but my real use-case is retrieving data based on search criteria. Once the user changes those criteria, it will update with the new result. However, I'm unable to pass those new criteria into componentDidMount so the page would refresh automatically every few seconds.
class App extends React.Component {
componentDidMount() {
const { text } = this.props //Redux store prop
setInterval(() => this.retrieveData(text), 3000)
}
retrieveData = (text) => {
let res = axios.post('/search', { text })
updateResults(res.data) //Redux action
}
render() {
const { text, results } = this.props
return (
<input text onChange={(e) => updateText(e.target.value)} />
<div>
{results.map((item) => <p>{item}</p>}
</div>
)
}
}
Because you are using componentDidMount and setTimeout methods your retrieveData is called only once with initial value of the text. If you would like to do it in your current way please use componentDidUpdate method which will be called each time the props or state has changed. You can find more information about lifecycle here https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/.
If you would like to use setInterval just like in the question, you just need to access props inside of retrieveData method instead of using an argument.
retrieveData = () => {
let res = post("/search", { text: this.props.text });
updateResults(res); //Redux action
};
You can find working example for both cases here https://codesandbox.io/s/charming-blackburn-khiim?file=/src/index.js
The best solution for async calls would be to use some kind of middleware like https://github.com/reduxjs/redux-thunk or https://redux-saga.js.org/.
You have also small issue with input, it should be:
<input type="text" value={text} onChange={(e) => updateText(e.target.value)} />

useEffect simulating componentWillUnmount does not return updated state

I have a functional component that initializes a state with useState, then this state is changed via an input field.
I then have a useEffect hook simulating componentWillUnmount so that, before the component unmounts, the current, updated state is logged to the console. However, the initial state is logged instead of the current one.
Here is a simple representation of what I am trying to do (this is not my actual component):
import React, { useEffect, useState } from 'react';
const Input = () => {
const [text, setText] = useState('aaa');
useEffect(() => {
return () => {
console.log(text);
}
}, [])
const onChange = (e) => {
setText(e.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
</div>
)
}
export default Input;
I initialize the state as "initial." Then I use the input field to change the state, say I type in "new text." However, when the component in unmounted, "initial" is logged to the console instead of "new text."
Why does this happen? How can I access the current updated state on unmount?
Many thanks!
Edit:
Adding text to useEffect dependency array doesn’t solve my problem because in my real-world scenario, what I want to do is to fire an asynchronous action based on the current state, and it wouldn’t be efficient to do so everytime the “text” state changes.
I’m looking for a way to get the current state only right before the component unmounts.
You've effectively memoized the initial state value, so when the component unmounts that value is what the returned function has enclosed in its scope.
Cleaning up an effect
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect. In our example, this means a new
subscription is created on every update. To avoid firing an effect on
every update, refer to the next section.
In order to get the latest state when the cleanup function is called then you need to include text in the dependency array so the function is updated.
Effect hook docs
If you pass an empty array ([]), the props and state inside the effect
will always have their initial values. While passing [] as the second
argument is closer to the familiar componentDidMount and
componentWillUnmount mental model, there are usually better solutions
to avoid re-running effects too often.
This means the returned "cleanup" function still only accesses the previous render cycle's state and props.
EDIT
useRef
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.
...
It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.
Using a ref will allow you to cache the current text reference that can be accessed within the cleanup function.
/EDIT
Component
import React, { useEffect, useRef, useState } from 'react';
const Input = () => {
const [text, setText] = useState('aaa');
// #1 ref to cache current text value
const textRef = useRef(null);
// #2 cache current text value
textRef.current = text;
useEffect(() => {
console.log("Mounted", text);
// #3 access ref to get current text value in cleanup
return () => console.log("Unmounted", text, "textRef", textRef.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
console.log("current text", text);
return () => {
console.log("previous text", text);
}
}, [text])
const onChange = (e) => {
setText(e.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
</div>
)
}
export default Input;
With the console.log in the returned cleanup function you'll notice upon each change in the input the previous state is logged to console.
In this demo I've logged current state in the effect and previous state in the cleanup function. Note that the cleanup function logs first before the current log of the next render cycle.

Resources