Value is not written to this.state - reactjs

On a React page I have a method by means of which I'm trying to set the state. However, it doesn't seem to work (see the console.log). What am I doing wrong?
editPlan = (cell, row) => {
return (
<img
src={edit}
alt="edit"
onClick={() => {
console.log(row.id); // prints 2
this.setState({ editId: row.id, openEditModal: true });
console.log(JSON.stringify(this.state.editId)); // prints "". I would expect 2.
console.log(JSON.stringify(this.state.openEditModal)); // prints false. I would expect true.
this.props.getPlan(row.id);
}}
/>
);
};

setState works asynchronously -- it just queues up your desired state change and will get to it later to optimize how your component will re-render.
You can pass a callback to the function as the last argument which will be called when the state change has occurred.
this.setState(
{ editId: row.id, openEditModal: true },
() => {
console.log(JSON.stringify(this.state.editId));
console.log(JSON.stringify(this.state.openEditModal));
}
);
See: https://reactjs.org/docs/react-component.html#setstate

You can try using the state callback function because setState works asynchronously
this.setState({ editId: row.id, openEditModal: true },()=>{
console.log(JSON.stringify(this.state.editId));
console.log(JSON.stringify(this.state.openEditModal));
this.props.getPlan(row.id);
});

Related

Wrong order of selectedItem while using onChange in a dropdown & using material-ui

Well, I have a dropdown and I want to use the onChange() selectedItem to then call an API and render the output using map.
My code looks something like this:
TaskSideBar.js
const taskAPI = 'https://pokeapi.co/api/v2/';
export default class TaskSideBar extends Component {
constructor(props) {
super(props);
this.state = {
pokeData: [],
pokeIndex: null,
isLoading: false,
error: null,
};
this.handleDropdownChange = this.handleDropdownChange.bind(this);
}
componentDidMount() {
}
handleDropdownChange(e) {
this.setState({selectedValue: e.target.value})
fetch(taskAPI + this.state.selectedValue )
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('something is wrong');
}
})
.then (data => this.setState({ pokeData: data.results, pokeIndex: 0, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
}
render () {
const { pokeData, pokeIndex, isLoading, error, selectedValue, renderRow } = this.state;
const classes = this.props;
return (
<>
<Tabs tabs={['Reports', 'Graphs', 'Sheets']} className={classes.sidebarTabs}>
<>
<h3>Reports</h3>
<List source={reportItems} />
</>
<>
<h3>Graphs</h3>
<Dropdown source={graphItems} />
</>
<>
<h3>Sheets</h3>
<select id="dropdown" onChange={this.handleDropdownChange} className={classes.taskList}>
<option value="">select </option>
<option value="berry">Pokemon Berry</option>
<option value="ability">Pokemon Abilities</option>
<option value="version">Version Info</option>
</select>
</>
</Tabs>
<div>
selected sheet is: {this.state.selectedValue}
{
pokeData.map(hit =>
<div key={hit.name}>
<p> {hit.name} {hit.url} </p>
</div>
)
}
</div>
</>
);
}
}
What is actually happening is, once the page renders and I select 'berry', I get an error: 'https://pokeapi.co/api/v2/undefined': 404. So that means for some reason, the selectedValue was not set to 'berry'. However, if I then go on and select 'ability', it renders the pokeData.map but shows me the results for 'https://pokeapi.co/api/v2/berry' when it should be showing me the data for 'https://pokeapi.co/api/v2/ability' and this keeps happening on each select. It seems like the index is off by -1. I have a follow-up question as well, but I'd appreciate if someone can help me understand this. Thanks.
So, i have a couple of problems here:
Fix the issue with the index on the selectedItem, which is undef for the first selection and is set to index-1 on the next selections.
Perform the same thing using dropdown using material-ui. In which I do something like this:
const dropdownItems: [
'item1',
'item2',
'item3',
];
and the dropdown looks like:
<dropdown> source={dropdownItems} onChange={this.handleDropdownChange} </dropdown>
How do i make this work? It doesn't work as shown above.
The problem is that setState is async. The value is not yet set when you call the fetch and the previous value will be used. For the first time that's undefined.
Either use the value directly in your call from the event or use the setState callback as second parameter to trigger the API call like this.
this.setState({selectedValue: e.target.value}, () => fetch(...)...)
It should also work the same for the drop down.
Hope this helps.
The setState method is async which means that you cannot guarantee it has finished before the fetch method is called and hence the value can sometimes be undefined.
fetch(taskAPI + this.state.selectedValue)
A simple solution would be to use e.target.value in the fetch request too or supply a callback function to the setState that fetches the information once the state has been set.
For more information on setState see - https://reactjs.org/docs/react-component.html#setstate
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
Hope the above clears up the issue!
~Fraz

Dropdown select component state one step behind

I have no clue why the selected dropdown value is one step behind in the URL search params string. My url is like this: http://localhost/?dropdownsel=. Below is my code:
//App.js
//update params value
function setParams({ dropdownsel }) {
const searchParams = new URLSearchParams();
searchParams.set("dropdownsel", dropdownsel);
return searchParams.toString();
}
class App extends Component {
state = {
dropdownsel: ""
};
//update url params
updateURL = () => {
const url = setParams({
dropdownsel: this.state.dropdownsel
});
//do not forget the "?" !
this.props.history.push(`?${url}`);
};
onDropdownChange = dropdownsel => {
this.setState({ dropdwonsel: dropdownsel });
this.updateURL();
};
render() {
return (
<Dropdownsel
onChange={this.onDropdownselChange}
value={this.state.dropdownsel}
/>
);
}
}
Below is dropdownsel component code:
//Dropdownsel.js
const attrData = [{ id: 1, value: AA }, { id: 2, value: BB }];
class Dropdownsel extends Component {
onDropdownChange = event => {
this.props.onChange(event.target.value);
};
render() {
return (
<div>
<select value={this.props.value} onChange={this.onDropdownChange}>
<option value="">Select</option>
{attrData.map(item => (
<option key={item.id} value={item.value}>
{" "}
{item.name}
</option>
))}
</select>
</div>
);
}
}
export default Dropdownsel;
Thanks for formatting my code. I don't know how to do it every time when I post question. I figured it out myself. I need to make a call back function for updateURL() because the setState() is not executed immediately. so my code should be revised like below:
onDropdownChange = (dropdownsel) => {
this.setState({ dropdwonsel:dropdownsel }, ()=>{this.updateURL();
});
};
The problem occurs because this.setState is asynchronous (like a Promise or setTimeout are)
So there are two workarounds for your specific case.
Workaround #1 - using a callback
Use the callback option of this.setState.
When you take a look at the declaration of setState, it accepts an "optional" callback method, which is called when the state has been updated.
setState(updater[, callback])
What it means is that, within the callback, you have an access to the updated state, which was called asynchronously behind the scene by React.
So if you call this.updateURL() within the callback, this.state.dropdownsel value will be the one you are expecting.
Instead of,
this.setState({ dropdwonsel: dropdownsel });
this.updateURL();
Call this.updateURL in the callback.
// Note: '{ dropdwonsel }' is equal to '{ dropdwonsel: dropdwonsel }'
// If your value is same as the state, you can simplify this way
this.setState({ dropdwonsel }, () => {
this.updateURL()
});
Workaround #2 - passing the new value directly
You can also pass the new value directly as an argument of this.updateURL() (which might make testing easier as it makes you method depend on a value, which you can fake).
this.setState({ dropdwonsel });
this.updateURL(dropdownsel );
Now your this.updateURL doesn't depend on the this.state.dropdownsel, thus you can can use the arg to push the history.
//update url params
updateURL = dropdownsel => {
const url = setParams({
dropdownsel
});
//do not forget the "?" !
this.props.history.push(`?${url}`);
};

onClick method runs setState multiple times in a row, negating intended purpose

I'm attempting to have my onClick method handleClick set the state of the active and groupCardInfo properties. active in particular is a boolean, and I'm using this bool value to determine whether a side menu item should be expanded or not.
SideMenuContainer component that calls handleClick:
render() {
if (this.props.active == true){
return (
<ParentContainer>
<p onClick={this.props.handleClick(this.props.properties)}>{this.props.parentName}</p>
<NestedContainer>
{this.props.properties.map(propertyElement => {
return (
<NestedProperty onClick={() => { this.props.changeInfoList(propertyElement.name, propertyElement.data_type, propertyElement.app_keys)}} >
{propertyElement.name}
</NestedProperty>
);
})}
</NestedContainer>
</ParentContainer>
);
}
The issue is that clicking that <p> results in handleClick running multiple times. so rather than toggling the active value from false to true, it toggles it back and forth multiple times so that it goes from false back to false again.
What's incorrect with the way I'm structuring this method in the parent App.js that's causing this?:
handleClick(properties){
console.log("toggle click!")
// this.setState({active : !this.state.active});
this.setState({
active: !this.state.active,
groupedCardInfo: properties
})
console.log("the active state is now set to: " + this.state.active)
}
It's because you are invoking the function in event handler. The first time render runs it will execute your event handler. You can do this like your other onClick handler:
<p onClick={() => { this.props.handleClick(this.props.properties) }}>{this.props.parentName}</p>
Or you can do it like this:
<p onClick={this.props.handleClick}>{this.props.parentName}</p>
But then you would have to change how you reference properties in your click handler. Like this:
handleClick(){
const properties = this.props.properties
this.setState({
active: !this.state.active,
groupedCardInfo: properties
})
console.log("the active state is now set to: " + this.state.active)
}
Try using arrow function, like you did on the other onClick:
<p onClick={() => this.props.handleClick(this.props.properties)}>
Calling this.props.handleClick... while rendering, invokes it.
Then, setting the state, makes the component to re render.

How to set a state array with values from TextField using onchange

I am new to react and am trying to add string values in an array. I am using Material-UI objects.
My state has
this.state: {
roles: []
}
A button pushes an undefined element in roles, incrementing its length.
clickAddRole = () => {
this.setState({roles: this.state.roles.concat([undefined]) });
};
So now we have some length to the roles array.
The Textfield is generated with
this.state.roles.map((item, i)=> {
return (
<TextField id={'roles['+i+']'} label={'role '+i} key={i} onChange={this.handleChange('roles['+i+']')} />
)
})
the onchange event is handled as below
handleChange = name => event => {
console.log(name);
this.setState({[name]: event.target.value});
console.log(this.state.roles);
}
The console.log statements generate output like
roles[0]
[undefined]
I expect
roles[0]
["somedata"]
what is going wrong here? The data does not get set in the roles array.
The whole code file is
const styles = theme => ({
error: {
verticalAlign: 'middle'
},
textField: {
marginLeft: theme.spacing.unit,
marginRight: theme.spacing.unit,
width: 300
},
submit: {
margin: 'auto',
marginBottom: theme.spacing.unit * 2
}
})
class AddModule extends Component {
constructor() {
super();
this.state = {
roles:[],
open: false,
error: ''
}
}
clickSubmit = () => {
const module = {
roles: this.state.roles || undefined
}
create(module).then((data) => {
if (data.error) {
this.setState({error: data.error})
} else {
this.setState({error: '', 'open': true});
}
})
}
clickAddRole = () => {
this.setState({roles: this.state.roles.concat([undefined]) });
};
handleChange = name => event => {
console.log(name);
this.setState({[name]: event.target.value});
console.log(this.state.roles);
}
render() {
const {classes} = this.props;
return (
<div>
<Button onClick={this.clickAddRole} >Add Role</Button>
{
this.state.roles.map((item, i)=> {
return (
<TextField className={classes.textField} id={'roles['+i+']'} label={'role '+i} key={i} onChange={this.handleChange('roles['+i+']')} />
)
})
}
</div>
)
}
}
I think you're making the whole code a bit overcomplicated creating names for each input field. What I would do is change the handleRolesChange or handleChange (not really sure if you changed its name) method so that it takes the index instead of a name.
handleRolesChange = index => event => {
const { roles } = this.state;
const newRoles = roles.slice(0); // Create a shallow copy of the roles
newRoles[index] = event.target.value; // Set the new value
this.setState({ roles: newRoles });
}
Then change the render method to something like this:
this.state.roles.map((item, index) => (
<TextField
id={`roles[${index}]`}
label={`role ${index}`}
key={index}
onChange={this.handleRolesChange(index)}
/>
))
Guy I have the issue (maybe temporarily).
I an array-element is a child of the array. so changing the data in the array-element does not need setState.
So this is what I did....
handleRolesChange = name => event => {
const i = [name];
this.state.roles[i]=event.target.value;
}
I also change the Textfield onchange parameter to
onChange={this.handleRolesChange(i)}
where i is the index starting from zero in the map function.
All this works perfectly as I needed.
However, if you think that I have mutated the roles array by skipping setState, I will keep the Question unanswered and wait for the correct & legitimate answer.
Thanks a lot for your support guys.
We must try and find the solution for such basic issues. :)
Are you positive it's not being set? From React's docs:
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
Usually logging state in the same block you set the code in will print the previous state, since state has not actually updated at the time the console.log fires.
I would recommend using React Dev Tools to check state, instead of relying on console.log.

callback in setState doesn't seem to work

I have the following:
addTodo() {
const text = prompt("TODO text please!")
this.setState({todos:[...this.state.todos,
{id:id++,
text: text,
checked:false}]})
console.log(this.state)
}
the console shows an empty array which makes sense as setState is asyncronous. I change the function to use a callback:
addTodo() {
const text = prompt("TODO text please!")
this.setState(function(prevState){
return {todos: [...prevState.todos,
{id: id++,
text: text,
checked: false} ]}
})
console.log(this.state)
}
console.log is still showing an empty array. Doesn't the use of the callback update setState?
setState function's second argument should be the function which need to be called after setting the state. So you should pass callback as second argument like this
this.setState({
todos:[
...this.state.todos,
{id:id++,text: text,checked:false}
]
},() => {console.log(this.state)})

Resources