React - State not updated or getting reset to old state - javascript

I am trying to build a chat application with the functionality of input field which can be used as filter for chat_groups array which is in the state as chat_groups. Here is how my code looks:
constructor(props) {
super(props);
this.state = {
currUserId: "--id--",
chats: [],
chat_groups: [],
users: [],
};
}
.
.
.
<input
className="chat-group__search__input"
placeholder="Search for group..."
onChange={(ev) => {
console.log(ev.currentTarget.value);
var thatState = this.state;
thatState.chat_groups = thatState.chat_groups.map(
(gp) => {
gp["visible"] = gp.group_name
.toLowerCase()
.includes(ev.currentTarget.value);
return gp;
}
);
// getting correct state in thatState variable
this.setState(thatState);
}}
/>
// getting old state in setState callback and componentDidUpdate lifecycle
The weird problem is I am getting the correct value in thatState variable before setting state. But after setState function is called, if I try to check the state in setState callback or componentDidUpdate lifecycle, I am getting the old state only.
I tried that for keydown and change events also. So, seems to be less of an issue of event as well.
I would like to know if some issue in the code is evident or there is something that I can do to debug the issue.
Edit: After changes, my current onChange looks as below, but the issue is still there; the setState function does not seem to change the state as I can see only the old state in componentDidUpdate lifecycle and setState callback.
onChange={(ev) => {
console.log(ev.currentTarget.value);
let chat_groups = this.state.chat_groups.map((gp) => ({
...gp,
visible: gp.group_name
.toLowerCase()
.includes(ev.currentTarget.value),
}));
console.log(
"Before",
chat_groups.map((gp) => gp.visible)
);
this.setState({ chat_groups: chat_groups });
}}

The problem is that you are mutating the state.
When you do var thatState = this.state; the reference is still the same for both the objects. So automatically when you update thatState.chat_groups you are updating/mutating state as well.
Change your onChange method to like below
onChange = ev => {
console.log(ev.currentTarget.value);
let { chat_groups } = this.state;
chat_groups = chat_groups.map(gp => ({
...gp,
visible: gp.group_name.toLowerCase().includes(ev.currentTarget.value)
}));
this.setState(state => ({
...state,
chat_groups
}));
};
//Other code
//....
//....
<input
className="chat-group__search__input"
placeholder="Search for group..."
onChange={this.onChange} />
I suspect there's one problem while checking the group_name with the input value i.e., you are converting the group_name to lower case using gp.group_name.toLowerCase() but the input value you are not converting to lower case. This could be one issue why the visible attribute value is not getting updated. So in the below snippet I have converted the input value also to lower case while comparing.
Here, below is a runnable snippet with your requirement. Doing console.log in the setState's callback function and the state is getting updated.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
currUserId: "--id--",
chats: [],
chat_groups: [{
group_name: "Group One"
}, {
group_name: "Group Two"
}],
users: []
}
}
onChange = ev => {
console.log(ev.currentTarget.value);
let {
chat_groups
} = this.state;
chat_groups = chat_groups.map(gp => ({
...gp,
visible: gp.group_name.toLowerCase().includes(ev.currentTarget.value.toLowerCase())
}));
this.setState(state => ({
...state,
chat_groups
}), () => { console.log(this.state.chat_groups); });
};
render() {
return <input
placeholder="Search for group..."
onChange={this.onChange} />
}
}
ReactDOM.render(<App />, document.getElementById("react"));
.as-console-wrapper {
max-height: 100% !important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="react"></div>

No, don't do this var thatState = this.state it's just an object it will easily get mutate and you will not get the update as for react state change never occured.
instead do this var { chat_groups } = this.state and then use it further and inlast set the state this.setState({ chat_groups: chat_groups }) also try to avoid the mutation as much as possible means copy the values of chat_groups also

Seems like you are trying to manipulate state directly which is a big no in React.
onChange={(ev) => {
this.setState({
chat_groups: this.state.chat_groups.map((gp) => {
gp["visible"] = gp.group_name
.toLowerCase()
.includes(ev.currentTarget.value);
return gp;
})
});
}}

State should only be updated using the setState method.
You are mutating the state directly in your code above - this isn't recommended. You would get unexpected results and it's not predictable.
This is how you should do it - create a new updated object and pass it to the setState.
onChange={(ev) => {
console.log(ev.currentTarget.value);
const updatedChatGroups = this.state.chat_groups.map((gp) => {
const visible = gp.group_name.toLowerCase().includes(ev.currentTarget.value);
return {
...gp,
visible,
};
});
// Update the modified object using this.setState().
this.setState({ chat_groups: updatedChatGroups });
}}
Read More

Related

Why is the initial state being used when I try to update state

I have a react component that uses hooks for state. I have set the initial state for home to {location:null, canCharge: 'yes'}.
I then have a couple of subcomponents that call setHome() to update the pieces of the state they are responsible for.
One sets the location, and the other sets the canCharge property of the home state.
The setter for the ChargeRadioGroup works as expected, only updating the canCharge property and has no effect on the value of location.
The PlacesAutoComplete set however seems to have captured the initial state of home, and after setting a breakpoint inside, I see that it always is called with home: {location:null, canCharge:'yes'}.
I realize I could break this single state into two separate states, one for location and one for canCharge, but I'd like to understand why this is happening instead of implementing a workaround.
export default function VerticalLinearStepper() {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const [home, setHome] = useState({
location: null,
canCharge: "yes"
});
const [work, setWork] = useState({
location: null,
canCharge: "yes"
});
const steps = getSteps();
const handleNext = () => {
setActiveStep(prevActiveStep => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep(prevActiveStep => prevActiveStep - 1);
};
return (
<div className={classes.root}>
<Stepper activeStep={activeStep} orientation="vertical">
<Step>
<StepLabel>Where do you live?</StepLabel>
<StepContent>
<Box className={classes.stepContent}>
<PlacesAutocomplete
className={classes.formElement}
name={"Home"}
onPlaceSelected={location => setHome({ ...home, location })}
googleApiKey={"<API_KEY>"}
/>
<ChargeRadioGroup
className={classes.formElement}
label="Can you charge your car here?"
value={home.canCharge}
onChange={event =>
setHome({ ...home, canCharge: event.target.value })
}
/>
The code for the PlacesAutoComplete component can be seen here
I'm guessing this has something to do with the way that this component calls it's onPlaceSelected prop, but I can't figure out exactly what's going on, or how to fix it:
useEffect(() => {
if (!loaded) return;
const config = {
types,
bounds,
fields
};
if (componentRestrictions) {
config.componentRestrictions = componentRestrictions;
}
autocomplete = new window.google.maps.places.Autocomplete(
inputRef.current,
config
);
event = autocomplete.addListener("place_changed", onSelected);
return () => event && event.remove();
}, [loaded]);
const onSelected = () => {
if (onPlaceSelected && autocomplete) {
onPlaceSelected(autocomplete.getPlace());
}
};
Updating my original answer.
Instead of this:
onPlaceSelected={location => setHome({ ...home, location })}
This:
onPlaceSelected={newlocation => setHome( (prevState) => (
{ ...prevState, location:newlocation }
))}
The set state functions can take a value, and object or a function that receives the old state and returns the new state. Because setting state is sometimes asynchronous, object state with members getting set with different calls may result in captured variables overwriting new state.
More details at this link: https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3

Calling setState in callback of setState generate a weird bug that prevents input's value to be updated by onChange

I have a list of input to generate dynamically from an array of data I retrieve from an API.
I use .map() on the array to generate each of my input, and set value={this.state.items[i]} and the onChange property (with a modified handleChange to handle change on an array properly).
Now, I set in my constructor this.state = { items: [{}] }, but since I don't know how many items are going to be generate, value={this.state.items[i].value} crash since this.state.items[n] doesn't exist.
The solution is then to set each this.state.items[i] = {} (using Array.push for example) first, and then generate all the inputs.
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
})
https://jsfiddle.net/qzb17dut/38/
The issue with this approach is that this.state.items doesn't exist yet on value={this.state.items[i].value} and we get the error Cannot read property 'value' of undefined.
Thankfully, setState() comes with a handy second argument that allows to do something only once the state is set. So I tried this:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
}, () => this.setState({
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
}))
https://jsfiddle.net/qzb17dut/39/
(Update: Please have a look at this example that better illustrate the use case: https://jsfiddle.net/jw81uo4y/1/)
Looks like everything should work now right? Well, for some reason, I am having this very weird bug were value= doesn't update anymore like when you forget to set onChange= on an input, but here onChange= is still called, value= is just not updated making the field remaining not editable.
You can see on the jsfiddle the problem for each method. The first one doesn't have the state set yet, which would allow the input to be edited, but crash because the state value was not yet set. And the second method fix the first issue but introduce this new weird bug.
Any idea about what I am doing wrong? Am I hitting the limit of react here? And do you have a better architecture for this use case? Thanks!
What about this approach instead, where you set the state of the API values only and then, generate the input based on the state from the render via Array.prototype.map like so
constructor (props) {
this.state = {items: []}
}
async componentDidMount(){
const apiData = await fetchApiData()
this.setState({items: apiData})
}
handleChange = (value, index) => {
const items = this.state.items;
items[index].value = value;
this.setState({ items });
};
updateState = () => {
const items = this.state.items;
items.push({value: ''}); // default entry on our item
this.setState({ items });
};
// here ur state items is exactly same structure as ur apiData
onSubmit =()=> {
console.log('this is apiData now', this.state.items)
}
render () {
<button onClick={this.updateState}>update state with inputs</button>
<button onClick={this.onSubmit}>Submit</button>
{this.state.items.map((item, index) => (
<input
key={index}
value={item.value}
onChange={e => this.handleChange(e.target.value, index)}
/>
))}
}
here is the codesandbox code for it
https://codesandbox.io/s/icy-forest-t942o?fontsize=14
with this, it will generate the input based on the items on the state, which in turns have the click handler which updates the state.
Well if I understand correctly, apiData is assigned to state.items and then also used to generate the inputs array. That means that for your purpose apiData and state.items are equivalent. Why don't you use the third map argument like:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i, arr) => {
return <input key={i} value={arr[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
});
or the apiData array directly?

What does this error means? Render methods should be a pure function of props and state

I'm totally new in React and had stucked with this error.
Error occurs because of this method:
deleteItem = (index) => {
this.state.items.splice(index, 1),
this.setState({
items: this.state.items
})
}
I also using #babel/plugin-proposal-class-properties
If I change button onClick event to this:
onClick={this.deleteItem.bind(index)}
it starts working perfectly and i dunno why.
Whole code example
import React from 'react';
export default class extends React.Component{
constructor(props) {
super(props)
this.state = {
items: [1, 2, 3, 4, 5]
};
};
addItem = () => {
this.state.items.push('added');
this.setState({
items: this.state.items
});
}
deleteItem = (index) => {
this.state.items.splice(index, 1),
this.setState({
items: this.state.items
})
}
render(){
const list = this.state.items.map((item, index) => {
return <li key={index}>{item}
<button onClick={this.deleteItem(index)}>Remove</button>
</li>
});
return (
<div>
<ul>
{list}
</ul>
<button onClick={this.addItem}>Add</button>
</div>
)
}
}
How can i fix this?
As per the React docs:
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.
You are directly mutating the state, don't do it. Pass a new state object in the setState() method.
deleteItem = (index) => {
const newItems = this.state.items.filter((ele, idx) => idx != index),
this.setState({
items: newItems
})
}
Or you can use the Array.prototype.slice() function, which does not modify the original array as it takes a copy of it in the given range and returns it in a new array:
deleteItem = (index) => {
const newItems = [...s.slice(0, index), ...s.slice(index + 1)],
this.setState({
items: newItems
})
}
Also the this context is not set in your onClick handler, use a arrow function () => to capture the lexical this:
onClick={() => this.deleteItem(index)}
That is necessary as this points to the current execution context, if you don't bind this to the lexical this and when the callback is invoked the this won't be referring to your component instance anymore.
Also if you invoke deleteItem(index) immediately the handler will point to undefined instead of the deleteItem reference as the deleteItem(index) returns undefined.
You need to modify the addItem() method as well, where you should form a new array from the added element:
addItem = () => {
this.setState({
items: this.state.items.concat('added')
});
}
The error message refers to a common concept in functional programming - pure functions - which helps you write less buggy code. Pure functions have no side effects in that they don't assign new values to variables, and they don't mutate the state of the application. A famous example of a pure function is one that returns the square root of the argument. There's a test you can run in your mind: can you call the function multiple times with the same result/outcome? If it's a pure function, calling it multiple times does not change the result.
In your example, rendering the same content multiple times should be possible without any side effects. Rendering the content should not add or delete any items. If you want this pure function (render) to return a different result, you have to call it with different arguments which are the props and state.
The problem is that you're mutating your state directly, use filter instead:
deleteItem = (index) => {
this.setState(state => ({
items: state.items.filter((item, i) => index !== i)
}))
}
I've recently wrote a post about this and other issues when using React.
Also looking at your render code, when you do <button onClick={this.deleteItem(index)}>Remove</button>, it will execute deleteItem immediately and pass the result of that to onClick, however onClick expects a function. What you need to do is to wrap it in a function:
<button onClick={() => this.deleteItem(index)}>Remove</button>
We must not mutate states directly
Instead of
addItem = () => {
this.state.items.push('added');
this.setState({
items: this.state.items
});
}
Use
addItem = () => {
this.setState({
items: […this.state.items,'added']
});
}
And Instead of
deleteItem = (index) => {
this.state.items.splice(index, 1),
this.setState({
items: this.state.items
})
}
Use
deleteItem = (index) => {
this.setState(state => ({
items: state.items.filter((item, idx) => index !== idx)
}))
}
Use latest es6 or latter features of javascript as arrow function, object destructuring, spread operator and other

React doesn't render all components of array

I want to dynamically add Components, after clicking the "add" button.
For that, I created an array that consists of all the components, and add them on click.
My problem is, that it only renders one component, even though it consists of several ones.
My code looks like this:
class QuestionBlock extends React.Component {
constructor(props) {
super(props);
this.state = {answersArray: []};
}
addPossibleAnswer() {
this.state.answersArray.push(
<PossibleAnswers id={this.state.answersArray.length + 1}/>
)
this.forceUpdate();
}
componentWillMount() {
this.state.answersArray.push(
<PossibleAnswers id={this.state.answersArray.length + 1}/>
)
}
render() {
console.log(this.state.answersArray) // Grows after adding componenets, but they are not rendered.
return (
<div>
{this.state.answersArray}
<AddPossibleAnswer addPossibleAnswer={() => this.addPossibleAnswer()} />
</div>
);
}
}
If you see what I did wrong, I'd be really glad if you could help me out!
Instead of mutating state directly and adding JSX to it, you can instead keep raw data in your state and derive the JSX from that in the render method instead.
Example
class QuestionBlock extends React.Component {
state = { answers: 1 };
addPossibleAnswer = () => {
this.setState(({ answers }) => ({ answers: answers + 1 }));
};
render() {
return (
<div>
{Array.from({ length: this.state.answers }, (_, index) => (
<PossibleAnswers key={index} id={index} />
))}
<AddPossibleAnswer addPossibleAnswer={this.addPossibleAnswer} />
</div>
);
}
}
You don't interact with state like you do. Never mutate the state field. You need to use this.setState:
this.setState(prevState => ({answersArray: prevState.answersArray.concat([
<PossibleAnswers id={prevState.answersArray.length + 1}])}));
Having said that, it is also strange that you store components in state. Usually, you would store data and create the components based on the data in the render method.
You are directly pushing elements to the array without setState so the component won't re-render
Also avoid using tthis.forceUpdate() as much as you can in your application because this is not recommended much
You need to change your code like below. The recommended approach for dealing with arrays in react is using previous state and push to an array
addPossibleAnswer() {
this.setState(prevState => ({
answersArray: [...prevState.answersArray, <PossibleAnswers id={prevState.answersArray.length + 1}/>]
}));
}
componentWillMount() {
this.setState(prevState => ({
answersArray: [...prevState.answersArray, <PossibleAnswers id={prevState.answersArray.length + 1}/>]
}));
}
Also keep in mind that componentWillMount life cycle method is deprecated in react 16. So move the code to componentDidMount instead
Here is the corrected code
class QuestionBlock extends React.Component {
constructor(props) {
super(props);
this.state = {answersArray: []};
}
addPossibleAnswer() {
this.setState(prevState => ({
answersArray: [...prevState.answersArray, <PossibleAnswers id={prevState.answersArray.length + 1}/>]
}));
}
componentDidMount() {
this.setState(prevState => ({
answersArray: [...prevState.answersArray, <PossibleAnswers id={prevState.answersArray.length + 1}/>]
}));
}
render() {
const { answersArray } = this.state;
return (
<div>
{answersArray}
<AddPossibleAnswer addPossibleAnswer={() => this.addPossibleAnswer()} />
</div>
);
}
}

React this.setState not updating my value, on firebase changes are showing

I have a problem with my code. I am trying to make a notes in react + fireabse. Adding notes to fireabse works and setState shows them, but if I want to change the value of the note, second setState does not change it but in firebase the note will change its value.
Here is my code
constructor() {
super();
this.app = firebase.initializeApp(DB_CONFIG);
this.database = this.app.database().ref().child('notes');
this.state = {
notes: [],
};
}
componentDidMount() {
this.database.on('child_added', snap => {
this.state.notes.push(new Note(snap.key, snap.val().noteContent));
this.setState({
notes: this.state.notes
});
});
this.database.on('child_changed', snap => {
this.state.notes.forEach(note => {
if(snap.key === note.id) {
note.id = snap.key;
note.noteContent = snap.val().noteContent;
}
});
this.setState({
notes: this.state.notes,
});
});
}
addNote(note) {
this.database.push().set({
noteContent: note,
});
}
changeNote(id, note) {
this.database.child(id).update({
noteContent: note,
});
}
render() {
return (
<div>
<div> {
this.state.notes.map(note => {
return (
<NoteComponent noteContent={note.noteContent}
noteId={note.id}
key={note.id}
changeNote={this.changeNote.bind(this)}>
</NoteComponent>
)
})
}
</div>
<div>
</div>
<NoteFormComponent
addNote={this.addNote.bind(this)}>
</NoteFormComponent>
</div>
);
}
Thanks for help.
Problem lines:
this.state.notes.push(new Note(snap.key, snap.val().noteContent));
or
this.state.notes.forEach(note => {
if(snap.key === note.id) {
note.id = snap.key;
note.noteContent = snap.val().noteContent;
}
});
this.setState({
notes: this.state.notes,
});
You cannot change the value of the state like this. You HAVE to use setState.
To fix this you need to:
copy(deepclone) the state array (You shoudl use ImmutableJS but this will work for testing: const copiedArray = JSON.parse(JSON.stringify(array)))
Do the changes to the copied array copiedArray.
setState('notes', copiedArray)
Other suggestions:
I would suggest to you to do the following. Isolate the firebase layer from the viewing layer. Mixing responsibilities of the component with the db communication is not recommanded.
After you do that. you will pass the list of note from outside the component. And any method working on the database(firebase in you case will come as a param also.)
// Notes Container
const notes = FireBaseConnector.getNotes();
const {
add,
edit,
delete,
} = FireBaseConnector
<Notes notes={notes}
onAdd={add}
onEdit={edit}
onRemove={delete}
/>
You should do something like this:
const newNotes = [...this.state.notes]
const newNode = {
id: snap.key,
noteContent: snap.val().noteContent,
}
newNotes.push(newNode)
this.setState({
notes: newNotes,
});
you cannot just push you need to replace the node array with the new array. immutability need to be followed to tell react to rerender

Categories