ReactJS change value of nested state - reactjs

I have a form in which inputs have an initial value (which is different for each input). I set the value by saving the data in a state array, like so:
function createData(header, content) {
id += 1;
return {header, content, id};
}
class ExampleForm extends React.Component {
constructor(props) {
super(props);
this.state = {
data : [
createData( Header1, Content1),
createData( Header2, Content2),
createData( Header3, Content3),
]
}
Once the user startes typing something into the input field, I need to change the value of the state in in question in the corresponding array, however, I'm having trouble correctly targeting said state.
This is what I'm currently doing:
handleInputChange = (value, target) => {
const selectedArray = {...this.state.data[target]};
selArray.header = value;
this.setState(selArray);
};
}
(Note: value is the new value, target is the index)
However, it doesn't work because when I console.log() the new value, it still returns the old value.
I also read several other questions on here (with the help of with I wrote the code for what I'm doing right now in the function that changes the value), however, that did not work.
Other questions I read include React: Update nested state? , however, I could not figure out where they got getPersonsData() from, for instance. Does anyone know what I'm doing wrong in my current code? How do I fix it?

Update the state like this:
handleInputChange = (value, target) => {
const data = [...this.state.data];
data[target].header = value;
this.setState({ data });
};
To check the updated state value, use callback method, like this:
handleInputChange = (value, target) => {
const data = [...this.state.data];
data[target].header = value;
this.setState(
{ data },
() => console.log('updated state', this.state.data)
);
};
For more details about asyn behaviour of setState check this answer: Why calling setState method doesn't mutate the state immediately?

Related

Avoid looping through map if single item change

I have a List component as shown bellow. Component renders list of Items and listens for item changes using websocket (updateItems function). Everything works fine except that I noticed that when a single item change my renderItems function loops through all of items.
Sometimes I have more than 150 items with 30 updates in a second. When this happens my application noticeable slows down (150x30=4500 loops) and when another updateItems happens after, its still processing first updateItems. I implemented shouldComponentUpdate in Items component where I compare nextProps.item with this.props.item to avoid unnecessary render calls for items that are not changed. Render function is not called but looks like that just call to items.map((item, index) slowing down everything.
My question is, is there a way to avoid looping through all items and change only the one that updated?
Note that other object data are not changed in this case, only items array within object.
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
object: null, // containing items array with some other data
// such as objectId, ...
};
}
componentDidMount() {
// call to server to retrieve object (response)
this.setState({object: response})
}
renderItems= (items) => {
return items.map((item, index) => {
return (
<Item key={item.id} item={item}/>
);
});
}
// this is called as a websocket onmessage callback
// data contains change item that should be replaced in items array
updateItems = data => {
// cloning object here in order to avoid mutation of its state
// the object does not contains functions and null values and cloning
// this way works in my case
let cloneObject = JSON.parse(JSON.stringify(this.state.object));
let index = // call to a function to get index needed
cloneObject.items[index] = data.change;
this.setState({object: cloneObject});
}
render() {
return (
this.state.object && {this.renderItems(this.state.object.items)}
);
}
}
First I would verify that your Item components are not re-rendering with a console.log(). I realize you have written that they don't in your description but I'm unconvinced the map loop is the total cause of the issue. It would be great if you posted your Component code because I'm curious if your render method is expensive for some reason as well.
The method you are currently using to clone your last state is a deep clone, it's not only slow but it will also cause each shallow prop compare to resolve true every time. (ie: lastProps !== newProps will always resolve true when using JSON.parse/stringify method)
To keep each item's data instance you can do something like this in your state update:
const index = state.items.findIndex(item => item._id === newItem._id);
const items = [
...state.items.slice(0, index),
newItem,
...state.items.slice(index + 1),
];
Doing this keeps all the other items intact, except for the one being updated.
Finally as per your question how to prevent this list re-rendering, this is possible.
I would do this by using moving the data storage out of state and into two redux reducers. Use one array reducer to track the _id of each item and an object reducer to track the actual item data.
Array structure:
['itemID', 'itemID'...]
Object structure:
{
[itemID]: {itemData},
[itemID]: {itemData},
...
}
Use the _id array to render the items, this will only re-render when the array of _ids is changed.
class List() {
...
render() {
return this.props.itemIds.map(_id => <Item id={_id} />);
}
}
Then use another container or better yet useSelector to have each item fetch its data from the state and re-render when it's data is changed.
function Item(props) {
const {id} = props;
const data = useSelector(state => state.items[id]);
...
}
You can try wrapping the child component with React.memo(). I had a similar problem with a huge form (over 50 controlled inputs). Every time I would've typed in an input all the form would've get re-rendered.
const Item = memo(
({ handleChange, value }) => {
return (
<>
<input name={el} onChange={handleChange} defaultValue={value} />
</>
);
},
(prevProps, nextProps) => {
return nextProps.values === prevProps.values;
}
Also, if you're passing through props a handler function as I did above, it's worth mentioning that you should wrap it inside a useCallback() hook to prevent recreation if the arguments to the function did not changed. Something like this:
const handleChange = useCallback(e => {
const { name, value } = e.target;
setValues(prevProps => {
const newProps = { ...prevProps, [name]: value };
return newProps;
});
}, []);
For your scenario I would recommend don't use state for your array rather create state for every individual element and update that accordingly. Something like this
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
individualObject: {}
};
}
object = response; // your data
renderItems= (items) => {
this.setState({
individualObject: {...this.state.individualObject, ...{[item.id]: item}
})
return items.map((item, index) => {
return (
<Item key={item.id} item={item}/>
);
});
}
updateItems = data => {
let cloneObject = {...this.object}
let index = // call to a function to get index needed
cloneObject.items[index] = data.change;
this.setState({
individualObject: {...this.state.individualObject, ...{[item.index]: item}
})
}
render() {
return (
this.renderItems(this.object)
);
}
}

How to setState from function parameter in React [duplicate]

This question already has answers here:
React this.setState is not a function
(16 answers)
Closed 3 years ago.
I am passing the name and value to handleChange function from child component.
Now I want to set the state to the value that matches the provided name.
But I do not know how to set it.
I tried this way(but it gives error - 'this.setState is not a function') :
class ParentComponent extends Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
this.state = {
loanAmount: 348600.48,
numberOfYears: 70,
}
handleChange(name, value) {
this.setState({ name: value },() => {
this.financeCalcualte();
});
}
The code of the child component is:
onChange = (event) => {
const {name, rawValue} = event.target;
this.props.handleChange(name, rawValue)
}
What is the correct syntax to set it?
In order to use this in a callback function, as told in the comments you have to bind it in the constructor or use an arrow function.
Also, since your name is a variable, you can't update your state like:
this.setState({ name: value }
because this creates a name property in your state, does not update the state variable which has the name as your incoming variable. So, you should use a computed property.
handleChange = (name, value) =>
this.setState({ [name]: value }, () => this.financeCalcualte());
It seems like you are looking for computed property name
handleChange(name, value) {
this.setState({ [name]: value },() => {
this.financeCalcualte();
});
}
If you have this function in a Class based component then you can update your current function to an arrow function like below.
handleChange = (name, value) => {
this.setState({ name: value },() => {
this.financeCalcualte();
});
}
The thing is you are accessing this without a context being given.
Another way can be you bind this to the function. Below is the example to do it the other way inside your constructor.
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
// Other code ....
}
EDIT
Also as mentioned in other answer and comments you have to use computed property name, you can not just use a variable name directly and set it to another variable.
this.setState({[name]:value})

Setting state value in react native

Here is what I am trying to do and examples I have found don't seem to set the value either. So I am sure it is something simple I am over-looking.
setViewedSubmission(subId: string) {
logger.log("pre:", this.state.position.candidates.find((c)=> {
return c.submissionId == subId
}).viewedSubmission) //returns original value
this.state.position.candidates.find((c)=> {
return c.submissionId == subId
}).viewedSubmission = true //expect the value to update
this.forceUpdate()
logger.log("post:", this.state.position.candidates.find((c)=> {
return c.submissionId == subId
}).viewedSubmission) //returns original value still
}
This is what I actually started with but also doesn't work: Based on the responses, this is closer to what I should be doing but still doesn't. What is the issue here?
let pos = Object.assign({}, this.state.position)
pos.candidates.find((c) => {
return c.submissionId == subId
}).viewedSubmission = true;
this.setState({ position: pos })
The only way to update the state and get the screen re-rendered is to use the setState method. Here a look:
let candidates = this.state.position.candidates;
candidates = candidates.map(candidate => {
if(candidate.submissionId === subId) candidate.viewedSubmission = true;
return candidate;
});
this.setState(prevState => ({
position: {
...prevState.position,
candidates
}
}));
I think you're looking at this backwards. React is basically a state machine which means the UI reflects the current status of the state.
Without delving too deep into your code you probably want to structure things something similar to the following:
class Demo extends React.Component {
constructor(props){
super(props)
this.state = {
viewed: false,
}
}
someBusinessLogic(viewed) {
this.setState({
...this.state,
viewed: viewed,
})
}
render() {
const { viewed } = this.state
if (viewed ) {
return <div>VIEWED</dive>
} else {
return <div>UNVIEWED</div>
}
}
}
...and somewhere else
...//this is terrible code, just an example of using the business logic
<Demo viewed='true' ref={r => this.demo1 = r} />
<Demo viewed='false'/>
<button onClick={e => { this.demo1.someBusinessLogic('viewed') }}>Set Viewed = 'viewed'</button>
...
Structuring the component this way means that you reflect upon the state rather than trying to manage when updates occur. setState(newState) will re-render if the state is different.
My guess is that the internal state of your component is the same.
If I misread your question and you are not seeing updates passed to the component make sure you are using the key and that it is updating or override shouldComponentUpdate which is where forceUpdate starts to be useful.
Hopefully this helps, you might consider providing more details about your implementation so the community can be more specific.
You can't alter state directly. It is immutable (If you plan on working with React, you should read more about that). When you say this.state.value = 1, it won't update. Instead, you should do this.setState({ value: 1 }). And, if there are more itens in state, you should do this.setState(p => ({ ...p, value: 1 }), since when you update state, you must return the entire new state object.

ReactJS: why is pushing value into this.state array causing type error?

I tried to push a value into a state array but I get an issue TypeError: Cannot read property 'state' of undefined at this.state.rows.push(a);
Know why? I am trying to push a new value into the array after i click a button.
App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
constructor() {
super();
this.state = {
name: '',
rows: ['hello',<p>gfdfg</p>,'mello']
}
}
handle(e){
e.preventDefault();
var a = "h";
this.state.rows.push(a);
alert("hello");
}
render() {
return (
<div className="App">
Button<br/>
<input type="submit" id="black" onClick={this.handle}/><br/>
<p>{this.state.rows}</p>
</div>
);
}
}
export default App;
There are couple of things that are wrong here:
you should NEVER change the state directly:
This is a big No No:
this.state.rows.push(a);
instead you should do something like this:
this.setState({ rows : [...this.state.rows, a] })
or without ES6:
const newArray = this.state.rows.slice();
newArray.push(a);
this.setState({ rows: newArray })
You should always replace the state with a new one.
this in a react component is not what you think it is, in order to make it work you can do one of two things:
a. change your method to an arrow function:
handle = (e) => {
e.preventDefault();
var a = "h";
this.state.rows.push(a);
alert("hello");
}
b. bind this to the method:
constructor() {
super();
this.state = {
name: '',
rows: ['hello',<p>gfdfg</p>,'mello']
}
this.handle = this.handle.bind(this);
}
the method handle does not have access to the context of the class i.e this; consider writing it as a fat arrow function
// class definition
handle = () => {
e.preventDefault();
var a = "h";
this.state.rows.push(a);
alert("hello");
}
render() {
// render logic
}
Having said this, mutating the state is not a good idea, consider using setState if you want your component to re-render as a result of state change
handle = () => {
e.preventDefault();
let { rows } = this.state;
var a = "h";
rows.push(a);
this.setState({
rows,
});
}
You are doing wrong, you have to use setState() method to push the value in the array:
handle = (e) => {
e.preventDefault();
var a = "h";
let tempRows = [...this.state.rows];
tempRows.push(a)
this.setState({rows:tempRows})
alert("hello");
}
You have two problems.
Event handlers require 'this' to be bound: https://reactjs.org/docs/handling-events.html So following this, you must either write: this.handle = this.handle.bind(this) in your contructor, or change handler to arrow function, if your build process supports transpilation of class fields.
React component will only update if component props change, or component state changes. Which is done by comparing references. In your case, when you push to the array, you are mutating the state, so the new reference is never created, and component does not re-render. If you want to verify that, just put console.log(this.state.rows) below this.state.rows.push(a) and you'll see that the array has received the new value, but component does not represent it. You need to use this.setState to create a new reference for your state, like so: this.setState({ rows: [...this.state.rows, a] })
Another way of returning a new array from the current array with additional elements and then pushing to state is to use the concat method.
Example :
this.setState({ users: this.state.users.concat(<Additonal Items Here>)}

Why does it display only the last value of the array, and not the whole?

Why does it display only the last value of the array, and not the whole?.
When you update the value in the database, When you update the value in the database, it outputs all
constructor(props) {
super(props);
this.name = this.props.name;
this.state = {[this.name] : []};
}
componentDidMount() {
let cardQuantity =
firebase.database().ref("Users").child(this.name);
cardQuantity.on('value',snap => {
snap.forEach((childSnapshot)=> {
let card = {text: childSnapshot.val(), id: childSnapshot.key};
this.setState({[this.name] :[card].concat(this.state[this.name])});
});
});
}
render(){
return (
this.state[this.name].map( card => <h2 key={card.id}>{card.text}</h2>)
);
}
setState() is async so you have to use callback form of setState like below:
this.setState(prevState => ({
[this.name]: [card].concat(prevState[this.name])
}));
Demo: https://codesandbox.io/s/zrq0xxq2px
It shows 2 versions based on your code:
fixed version that is using callback form of setState and displaying the whole list
unfixed version based on your code that is showing only the last element of an array
From the official React's documentation:
If the next state depends on the previous state, we recommend using
the updater function form, instead...

Resources