I want to make a loading indicator on user login, but for some reason the JSX conditional element does not update:
class LoginPage extends Component {
constructor (props) {
super(props)
this.state = {
waitingOnLogin: false,
...
}
this.handleLogin = this.handleLogin.bind(this)
}
handleLogin (event) {
event.preventDefault()
this.state.waitingOnLogin = true
this.props.userActions.login(this.state.email, this.state.password)
}
render() {
return (
<div className='up'>
<form onSubmit={e => this.handleLogin(e)}> ... </form>
{this.state.waitingOnLogin && <Spinner message='Logging you in'/>} // This does not appear
</div>
)
}
}
Why does the waitingOnLogin is being ignored by the JSX?
Don't mutate state directly use setState. setState calls for rerender and hence after that your change will reflect but with direct assignment no rerender occurs and thus no change is reflected. Also you should always use setState to change state
handleLogin (event) {
event.preventDefault()
this.setState({waitingOnLogin:true});
this.props.userActions.login(this.state.email, this.state.password)
}
Always use setState to update the state value, never mutate the state values directly, Use this:
handleLogin (event) {
event.preventDefault()
this.setState({ waitingOnLogin: true });
this.props.userActions.login(this.state.email, this.state.password)
}
As per DOC:
Never mutate this.state directly, as calling setState() afterwards may
replace the mutation you made. Treat this.state as if it were
immutable.
Check the details about setState.
Related
I have a complete running code, but it have a flaw. It is calling setState() from inside a render().
So, react throws the anti-pattern warning.
Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount
My logic is like this. In index.js parent component, i have code as below. The constructor() calls the graphs() with initial value, to display a graph. The user also have a form to specify the new value and submit the form. It runs the graphs() again with the new value and re-renders the graph.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
checkData: true,
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost !== nextProps.cost) {
this.setState({
checkData: true
});
}
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
if (this.props.cost.length && this.state.checkData) {
const tmp = this.props.cost;
//some calculations
....
....
this.setState({
theData: tmp,
checkData: false
});
}
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
<PieGraph theData={this.state.theData} />
</div>
);
}
}
The FormComponent is an ordinary form with input field and a submit button like below. It sends the callback function to the Parent component, which triggers the graphs() and also componentWillReceiveProps.
handleFormSubmit = (e) => {
this.props.onGpChange(this.state.value);
e.preventdefaults();
}
The code is all working fine. Is there a better way to do it ? Without doing setState in render() ?
Never do setState in render. The reason you are not supposed to do that because for every setState your component will re render so doing setState in render will lead to infinite loop, which is not recommended.
checkData boolean variable is not needed. You can directly compare previous cost and current cost in componentWillReceiveProps, if they are not equal then assign cost to theData using setState. Refer below updated solution.
Also start using shouldComponentUpdate menthod in all statefull components to avoid unnecessary re-renderings. This is one best pratice and recommended method in every statefull component.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost != nextProps.cost) {
this.setState({
theData: this.props.cost
});
}
}
shouldComponentUpdate(nextProps, nextState){
if(nextProps.cost !== this.props.cost){
return true;
}
return false;
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
{this.state.theData !== "" && <PieGraph theData={this.state.theData} />}
</div>
);
}
}
PS:- The above solution is for version React v15.
You should not use componentWillReceiveProps because in most recent versions it's UNSAFE and it won't work well with async rendering coming for React.
There are other ways!
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps is invoked right before calling the render
method, both on the initial mount and on subsequent updates. It should
return an object to update the state, or null to update nothing.
So in your case
...component code
static getDerivedStateFromProps(props,state) {
if (this.props.cost == nextProps.cost) {
// null means no update to state
return null;
}
// return object to update the state
return { theData: this.props.cost };
}
... rest of code
You can also use memoization but in your case it's up to you to decide.
The link has one example where you can achieve the same result with memoization and getDerivedStateFromProps
For example updating a list (searching) after a prop changed
You could go from this:
static getDerivedStateFromProps(props, state) {
// Re-run the filter whenever the list array or filter text change.
// Note we need to store prevPropsList and prevFilterText to detect changes.
if (
props.list !== state.prevPropsList ||
state.prevFilterText !== state.filterText
) {
return {
prevPropsList: props.list,
prevFilterText: state.filterText,
filteredList: props.list.filter(item => item.text.includes(state.filterText))
};
}
return null;
}
to this:
import memoize from "memoize-one";
class Example extends Component {
// State only needs to hold the current filter text value:
state = { filterText: "" };
// Re-run the filter whenever the list array or filter text changes:
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// Calculate the latest filtered list. If these arguments haven't changed
// since the last render, `memoize-one` will reuse the last return value.
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
I have component Inbox and having checkbox in it... But It works on third click... does not work on first and second click... setState works perfect but it does't re-render after setState
class Inbox extends PureComponent {
constructor(props){
this.state = {
checked: [true]
}
}
updateCheck(i, convId) {
const state = this.state.checked
state[i] = !state[i]
this.setState(state)
}
render() {
return (
<input type="checkbox" checked={this.state.checked[i]} onClick={() => this.updateCheck(i, conv._id)}/>
)
}
}
You are not really updating the state correctly. setting state like
this.setState(state, () => {
console.log(this.state, '787878787878778787')
})
does not update the checked state using state but adds keys with array indices to state like
{0: true, 1: false, conversationId: '', checked: [true, false]};
You are instead mutating the checked state yourself using
state[i] = !state[i]
To update the state correctly, you would write
updateCheck(i, convId) {
const checked = [...this.state.checked]
checked[i] = !checked[i]
this.setState({ checked }, () => {
console.log(this.state, '787878787878778787')
})
}
The problem in your approach arises because you mutate the original state directly, subsequent setState calls may replace the original change and hence you see that behaviour.
According to documentation
Never mutate this.state directly, as calling setState() afterwards may
replace the mutation you made. Treat this.state as if it were
immutable.
Working demo
Ok, i'll try and make this quick because it SHOULD be an easy fix...
I've read a bunch of similar questions, and the answer seems to be quite obvious. Nothing I would ever have to look up in the first place! But... I am having an error that I cannot fathom how to fix or why its happening.
As follows:
class NightlifeTypes extends Component {
constructor(props) {
super(props);
this.state = {
barClubLounge: false,
seeTheTown: true,
eventsEntertainment: true,
familyFriendlyOnly: false
}
this.handleOnChange = this.handleOnChange.bind(this);
}
handleOnChange = (event) => {
if(event.target.className == "barClubLounge") {
this.setState({barClubLounge: event.target.checked});
console.log(event.target.checked)
console.log(this.state.barClubLounge)
}
}
render() {
return (
<input className="barClubLounge" type='checkbox' onChange={this.handleOnChange} checked={this.state.barClubLounge}/>
)
}
More code surrounds this but this is where my problem lies. Should work, right?
I've also tried this:
handleOnChange = (event) => {
if(event.target.className == "barClubLounge") {
this.setState({barClubLounge: !this.state.barClubLounge});
console.log(event.target.checked)
console.log(this.state.barClubLounge)
}
So I have those two console.log()'s, both should be the same. I'm literally setting the state to be the same as the event.target.checked in the line above it!
But it always returns the opposite of what it should.
Same goes for when I use !this.state.barClubLounge; If it starts false, on my first click it remains false, even though whether the checkbox is checked or not is based off of the state!!
It's a crazy paradox and I have no idea whats going on, please help!
Reason is setState is asynchronous, you can't expect the updated state value just after the setState, if you want to check the value use a callback method. Pass a method as callback that will be get executed after the setState complete its task.
Why setState is asynchronous ?
This is because setState alters the state and causes re rendering. This can be an expensive operation and making it synchronous might leave the browser unresponsive.
Thus the setState calls are asynchronous as well as batched for better UI experience and performance.
From Doc:
setState() does not immediately mutate this.state but creates a
pending state transition. Accessing this.state after calling this
method can potentially return the existing value. There is no
guarantee of synchronous operation of calls to setState and calls may
be batched for performance gains.
Using callback method with setState:
To check the updated state value just after the setState, use a callback method like this:
setState({ key: value }, () => {
console.log('updated state value', this.state.key)
})
Check this:
class NightlifeTypes extends React.Component {
constructor(props) {
super(props);
this.state = {
barClubLounge: false,
seeTheTown: true,
eventsEntertainment: true,
familyFriendlyOnly: false
}
}
handleOnChange = (event) => { // Arrow function binds `this`
let value = event.target.checked;
if(event.target.className == "barClubLounge") {
this.setState({ barClubLounge: value}, () => { //here
console.log(value);
console.log(this.state.barClubLounge);
//both will print same value
});
}
}
render() {
return (
<input className="barClubLounge" type='checkbox' onChange={this.handleOnChange} checked={this.state.barClubLounge}/>
)
}
}
ReactDOM.render(<NightlifeTypes/>, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='app'/>
Since setState is a async function. That means after calling setState state variable does not immediately change. So if you want to perform other actions immediately after changing the state you should use callback method of setstate inside your setState update function.
handleOnChange = (event) => {
let inputState = event.target.checked;
if(event.target.className == "barClubLounge") {
this.setState({ barClubLounge: inputState}, () => { //here
console.log(this.state.barClubLounge);
//here you can call other functions which use this state
variable //
});
}
}
This is by-design due to performance considerations. setState in React is a function guaranteed to re-render Component, which is a costly CPU process. As such, its designers wanted to optimize by gathering multiple rendering actions into one, hence setState is asynchronous.
I am new to ReactJS and am unsuccessfully attempting to manage a state change. The initial state renders as expected, the state successfully changes, however the elements do not render afterwards. There are no errors in the DOM console to go off of. I've made sure to set the initial state in the constructor of the component class, and I've also tried binding the method I'm using in the constructor since I've read auto-binding is not a part of ES6. The relevant component code is as follows:
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
myIDs: Array(6).fill('0')
};
this.getMyIDs = this.getMyIDs.bind(this);
};
componentDidMount() {
var ids = this.getMyIDs();
ids.then((result)=> {
this.setState({ myIDs: result }, () => {
console.log(this.state.myIDs)
});
})
};
componentWillUnmount() {
this.setState({
myIDs: Array(6).fill('0')
});
};
getMyIDs() {
return fetch('/api/endpoint').then((response) =>{
return response.json();
}).then((myIDs) => {
return myIDs.result
})
};
render() {
return (
<Tweet tweetId={this.state.myIDs[0]} />
<Tweet tweetId={this.state.myIDs[1]} />
);
}
}
export default MyComponent
UPDATE: The 'element' being updated is the 'Tweet' component from react-twitter-widgets. Its source is here:
https://github.com/andrewsuzuki/react-twitter-widgets/blob/master/src/components/Tweet.js'
export default class Tweet extends React.Component {
static propTypes = {
tweetId: PropTypes.string.isRequired,
options: PropTypes.object,
onLoad: PropTypes.func,
};
static defaultProps = {
options: {},
onLoad: () => {},
};
shouldComponentUpdate(nextProps) {
const changed = (name) => !isEqual(this.props[name], nextProps[name])
return changed('tweetId') || changed('options')
}
ready = (tw, element, done) => {
const { tweetId, options, onLoad } = this.props
// Options must be cloned since Twitter Widgets modifies it directly
tw.widgets.createTweet(tweetId, element, cloneDeep(options))
.then(() => {
// Widget is loaded
done()
onLoad()
})
}
render() {
return React.createElement(AbstractWidget, { ready: this.ready })
}
}
As in React docs:
componentWillMount() is invoked just before mounting occurs. It is
called before render(), therefore calling setState() synchronously in
this method will not trigger an extra rendering. Generally, we
recommend using the constructor() instead.
Avoid introducing any side-effects or subscriptions in this method.
For those use cases, use componentDidMount() instead.
you should not use ajax calls in componentWillMount
call ajax inside: componentDidMount
another thing: why do you use
componentWillUnmount
the object will be removed no reason to have that call there.
The only issue that is present in your current code is that you are returning multiple Element component instances without wrapping them in an array of a React.Fragment or a wrapper div. With the latest version of react, you must write
render() {
return (
<React.Fragment>
<Element Id={this.state.myIDs[0]} />
<Element Id={this.state.myIDs[1]} />
</React.Fragment>
);
}
}
Also as a practice you must have your Async calls in componentDidMount instead of componentWillMount as the React docs also suggest. You might want to read this answer on where write async calls in React for more details
Another thing that you must remember while using prop Id in your Element component is that componentWillMount and componentDidMount lifecycle functions are only called on the initial Render and not after that, so if you are using this.props.Id in one of these function in Element component then you will not be able to see the update since the result of async request will only come later, check this answer on how to tacke this situation
I have nested form objects but I try to setState using one handler, somehow doesn't work
constructor() {
super()
this.state = {
form: {
name: '',
location: ''
}
}
}
handleFormInput = (event) => {
this.setState({
[this.state.form[event.target.name]]: event.target.value
})
setTimeout(() => {
console.log(this.state.form)
},50)
}
event.target.name can be name and location.
You cannot directly access and modify a dynamic state within the setState function, you would rather get a copy of the state object and modify it. Also as you may already know that setState is async and hence you have a setTimeout function which is not necessary since setState provides you a callback function which is executed when state has changed.
handleFormInput = (event) => {
var form = {...this.state.form}
form[event.target.name] = event.target.value;
this.setState({
form
}, () => {this.state.form})
}
I think this code is not working properly:
this.setState({
[this.state.form[event.target.name]]: event.target.value
})
alternative is :
handleFormInput = (event) => {
//New code
let obj = {}
obj[this.state.form[event.target.name]] = event.target.value
this.setState(obj,() => { console.log(this.state.form) })
}
to view updated state, use callback function param of this.setState
setState is asynchronous, your state will properly update somehow after the call. Then render will be called again. Try checking your new state in render or in componentWillUpdate : https://facebook.github.io/react/docs/react-component.html#componentwillupdate
You should never look for a change in your state right after changing your state