Updating one State with two fields? - reactjs

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}
});
)};

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 change state from another component without passing setState method

I have a custom table component and it has two props: items and columns. I want to implement a sorting feature. Sorting is not a big deal. I am sorting the items inside the table component but when items are sorted, also the state that stores the items must be changed which is outside of my table component. I don't want to pass setState method cause of my component is generic. It would be very useless if I pass setState method everywhere.
How do popular libraries solve this problem without need of pass a state-altering method? Do they copy the state to an internal state and then modify it or something? I hope I could explain my problem.
It might help to think in terms of controlled vs uncontrolled components. You may be familiar with this from core elements like <input>s, where you can either pass in a defaultValue prop, and let the input handle everything ("uncontrolled"), or you can pass in value and onChange and handle things yourself ("controlled"). You can design your table component as either a controlled component or uncontrolled component too.
Doing it as an uncontrolled component, you might pass in a prop that sets the initial sorting, but afterwards everything is handled by the table. The parent won't get notified and won't update its state:
const Parent = () => {
const [items, setItems] = useState(/* some array */);
return <MyTable items={items} defaultSort="asc" />
}
const MyTable = ({ items, defaultSort }) => {
const [sort, setSort] = useState(defaultSort ?? 'asc');
const sortedItems = useMemo(() => {
if (sort === 'asc') {
return [...items].sort(/* insert sort function here */)
} else {
return [...items].sort(/* insert sort function here */)
}
}, [items, sort]);
return (
<>
<button onClick={() => setSort(sort === 'asc' ? 'dsc' : 'asc')}>
Change Sort
</button>
{sortedItems.map(() => /* etc */)}
</>
)
}
If instead you do a controlled component, then the parent is in charge of the state, and the child just notifies the parent of relevant changes
const Parent = () => {
const [items, setItems] = useState(/* some array */);
const [sort, setSort] = useState('asc');
const sortedItems = useMemo(() => {
if (sort === 'asc') {
return [...items].sort(/* insert sort function here */)
} else {
return [...items].sort(/* insert sort function here */)
}
}, [items, sort]);
return <MyTable items={sortedItems} onSortToggled={() => setSort(sort === 'asc' ? 'dsc' : 'asc')} />
}
const MyTable = ({ items, onSortToggled}) => {
return (
<>
<button onClick={onSortToggled}>
Change Sort
</button>
{items.map(() => /* etc */)}
</>
)
}
If you add in some extra code to check for undefineds, it's possible to make your table support both controlled and uncontrolled modes, based on which set of props it is passed. But it should just be one or the other; you shouldn't try to have both components be managing state simultaneously, as this just adds opportunities for the states to get out of sync and bugs to be introduced.
the state that stores the items must be changed which is outside of my table component
If this is one of your requirements, then you're basically doing the controlled component version, and thus you must accept a function from the parent component which describes how to do so. The parent component is the only one who knows what state they have and how to update it.

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)} />

Hooks setter not setting state with object variable

I'm trying to build a simple tree menu with react. I found this video showing exactly what I want to achieve (his Codepen). I was able to implement his approach, but then, out of curiosity, tried to replicate the same using hooks. I ended up with this (simplified):
my Codepen
const App2 = () => {
const [selectedOptions, setSelectedOptions] = React.useState({});
React.useEffect(() => {
console.log(selectedOptions);
},[selectedOptions]);
const updateSelection = (sel) => {
console.log(sel)
setSelectedOptions(sel);
}
return (
<div className="wrapper">
<h1>Toppings</h1>
<OptionsList
options={options}
onChange={updateSelection}
selectedOptions={selectedOptions}
/>
</div>
);
}
const OptionsList = ({ selectedOptions, onChange }) => {
const handleCheckboxClicked = (selectedOptionId) => {
if(selectedOptions[selectedOptionId]){
delete selectedOptions[selectedOptionId];
} else {
selectedOptions[selectedOptionId] = {}
}
onChange(selectedOptions);
}
return (
<div>
<button onClick={() => (handleCheckboxClicked("chicken-id"))} >
Set Chicken
</button>
</div>
)
}
ReactDOM.render(<App2 />, document.querySelector('#app'));
The problem is setSelectedOptions(sel), inside the function updateSelection is not doing absolutely anything (and of course useEffect is not being fired).
I'm not able to figure out why. I put a console.log just above it to check whether the variable ("sel") was okay or not, but it seems fine. I tried hardcoding the value of "sel", {chicken-id: {}}, and it works when I do so.
The problem is that you are directly mutating the state object:
if(selectedOptions[selectedOptionId]){
delete selectedOptions[selectedOptionId];
} else {
selectedOptions[selectedOptionId] = {}
}
onChange(selectedOptions);
In both outcomes you alter the state object and then set it as itself, so naturally the useEffect will not fire as the setter has effected no actual change. You have to be careful of doing this in React as it's an easy way to end up with stale values. Re-renders are only triggered when there is a difference between the current state and the value passed by the setter, not whenever the values of the state change, for whatever reason.
A comment suggested that you use spread - ... - to destructure the state object so that you can set it as itself, but this seems like a bit of an anti-pattern to me especially as it leaves the state mutation in place. If you want to delete an entry from a state object then a general approach would be to clone the object first, mutate the clone, then set that as the new state:
const clone = Object.assign({}, selectedOptions);
if(clone[selectedOptionId]){
delete clone[selectedOptionId];
} else {
clone[selectedOptionId] = {}
};
onChange(clone);
A word of caution though, any nested objects inside this object will retain their references to the original state and could still mutate it. You would have to clone these nested objects as well to avoid the issue.

React Hooks: Function still has old state?

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!

Resources