The state is like props, but is completely internal to the component. It can be used only with class components, but not with functional components.
React
Let's take a look at example from React docs.
var container1 = document.getElementById('container-1');
const tick = () => {
const element = (
<div>
<h1>It's a dynamic clock !!</h1>
<h2>It is {new Date().toLocaleTimeString()}</h2>
</div>
);
ReactDOM.render(element, container1);
}
setInterval(tick, 1000);
In this example, we have a function called tick which is being called every second to update DOM with new time. Here we are calling the render method for every second to update the DOM. In practice, most React apps only call render only once. How do we do it, then? we use internal state of class components. Let's rewrite example, using class component.
Create a completely reusable Clock component.
var container2 = document.getElementById('container-2');
class Clock extends React.Component {
render() {
return (
<div>
<h1>Its a clock component</h1>
<h2>It is {this.props.date.toLocaleTimeString()}</h2>
</div>
)
}
}
setInterval(() => {
ReactDOM.render(
<Clock date={new Date()}/>, container2);
}, 1000);
We have now created a clock component which is rendered for every second. Take look at a clock component, its accepting a prop called, date and a new date object is being passed every time it is called. So far good, still the component is not effective because of 2 reasons.
- Clock is not updating on its own, we are updating it for every second by calling ReactDOM. render method.
- Clock is accepting an input called date. In real world clock works on its own, it is not told by anyone what the time is?
var container6 = document.getElementById('container-3');
class Clock extends React.Component {
// Es6 feature
constructor(props) {
super(props);
this.state = { date: new Date() };
}
// Lifecycle method triggerd when Component added to DOM.
componentDidMount() {
this.timerId = setInterval(() => this.tick(), 1000);
}
//Lifecycle method triggerd when Component removed from DOM.
componentWillUnmount() {
clearInterval(this.timerId);
}
tick() {
this.setState({
date: new Date()
})
}
render() {
return (
<div>
<h1>Its a self contained Independent Clock</h1>
<h2>It is {this.state.date.toLocaleTimeString()}</h2>
</div>
)
}
}
ReactDOM.render(<Clock />, container3);
Let's quickly recap what's going on and the order in which the methods are called:
1) When this.state
with an object including the current time. We will later update this state.
2) React then calls the Clock component's render()
method. This is how React learns what should be displayed on the screen. React then updates the DOM to match the Clock's render output.
3) When the Clock output is inserted in the DOM, React calls the componentDidMount()
lifecycle hook. Inside it, the Clock component asks the browser to set up a timer to call tick() once a second.
4) Every second the browser calls the tick() method. Inside it, the Clock component schedules a UI update by calling setState()
with an object containing the current time. Thanks to the setState() call, React knows the state has changed, and calls render() method again to learn what should be on the screen. This time, this.state.date in the render() method will be different, and so the render output will include the updated time. React updates the DOM accordingly.
5) If the Clock component is ever removed from the DOM, React calls the componentWillUnmount()
lifecycle hook so the timer is stopped.
Using State Correctly
There are three things you should know about setState()
.
Do Not Modify State Directly (From React Docs)
For example, this will not re-render a component:
// Wrong
this.state.date = new Date();
Instead, use setState()
// Correct
this.setState({date: new Date()});
this.state
is the constructor.
State Updates May Be Asynchronous
React may batch multiple setState() calls into a single update for performance.
It means that the setState
method can be asynchronous at times. Updating state multiple times within the same function may not give the desired output.
Create a button which will display green and red with even and odd clicks respectively:
var container4 = document.getElementById('container-4');
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
counterClass: 'btn-success'
}
this.increment = this.increment.bind(this);
}
increment() {
// increases counter by 1
this.setState({
counter: this.state.counter + 1
});
let counterClass = 'btn-danger';
// set counterClass based even or odd.
if(this.state.counter % 2 === 0) {
counterClass = 'btn-success';
}
//update counterClass variable in state.
this.setState({
counterClass: counterClass
});
}
render() {
return (
<div>
<a href='#' id='btn' className={`btn btn-sm ${this.state.counterClass}`}
onClick={this.increment} >
<h4>Counter: {this.state.counter}</h4>
</a>
</div>
)
}
}
ReactDOM.render(<Counter />, container4);
Let's quickly go through what we have done:
1) Created a Counter
component with internal state having counter
and counterClass
.
2) Created a onClick
event listener called increment and updating internal state.
3) Attached our Counter component to container 7.
So far good, our expectation from the component is to display either green or red button based on even or odd output, but the button doesn't behave like what we expected it to be, Reason is
setState
changes are updated in batches i.e. all the updates in a single function are batched and then updated at once. This is mainly for improving performance.
this.setState({
counter: this.state.counter + 1
});
This code will not update the counter variable immediately when you set. It is kept in queue and will execute once the function execution is done. As an effect
if(this.state.counter % 2 === 0) {
counterClass = 'btn-success';
}
counterClass state is set based on previous counter state, but not on updated counter state because state changes are batched and yet to be executed.
this.state
is accessed after calling setState can return the existing value and not the updated value.
There are couple of ways to deal with these kind of situations.
- Using callback function as a second parameter for setState method. This callback function is triggered once the state is updated. Let's change the example to use callback
var container5 = document.getElementById('container-5');
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
counterClass: 'btn-success'
}
this.increment = this.increment.bind(this);
}
increment() {
this.setState({
counter: this.state.counter + 1
}, function() { // callback triggered when state updated
let counterClass = 'btn-danger';
if(this.state.counter % 2 === 0) {
counterClass = 'btn-success';
}
this.setState({ counterClass: counterClass });
});
}
render() {
return (
<div>
<a href='#' id='btn' className={`btn btn-sm ${this.state.counterClass}`}
onClick={this.increment} >
<h4>Counter: {this.state.counter}</h4>
</a>
</div>
)
}
}
ReactDOM.render(<Counter />, container5);
Though it looks simple using callback is not recommended for state changes because code becomes more complicated if state changes are dependent on other state changes and also code becomes difficult to maintain.
- Instead of object
setState
also accepts a function with 2 parametersprevState
andnextProps
and returns updated state. This is the most recommended way for updating state. Let's change example to use this method
var container6 = document.getElementById('container-6');
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
counterClass: 'btn-success'
}
this.increment = this.increment.bind(this);
}
increment() {
this.setState((prevState, nextProps) => {
const state = prevState;
let counterClass = 'btn-danger';
state.counter = state.counter + 1;
if(state.counter % 2 === 0) {
counterClass = 'btn-success';
}
state.counterClass = counterClass;
return state;
})
}
render() {
return (
<div>
<a href="#" id="btn" className={'btn btn-md ' + this.state.counterClass}
onClick={this.increment} >
<h4>Counter: {this.state.counter}</h4>
</a>
</div>
)
}
}
ReactDOM.render(<Counter />, container6);
State updates are asynchronous only when setState is called in React context like onClick, onMouseEnter etc... If you call setState method outside React context, then every call to setState is synchronous
Lets change the example to implement onClick outside React context.
var container7 = document.getElementById('container-7');
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
counterClass: 'btn-success'
}
}
componentDidMount() {
const ele = document.getElementById('syncStateBtn');
// React cannot batch the state changes because eventlistener is not in react context.
ele.addEventListener('click', function() {
this.setState({
counter: this.state.counter + 1
})
let counterClass = 'btn-danger';
if(this.state.counter % 2 === 0) {
counterClass = 'btn-success';
}
this.setState({
counterClass: counterClass
});
}.bind(this))
}
render() {
return (
<div>
<a href="#" id="syncStateBtn" className={'btn btn-md ' + this.state.counterClass} >
<h4>Counter: {this.state.counter}</h4>
</a>
</div>
)
}
}
ReactDOM.render(<Counter />, container7);
Instead of using React's onClick event listener, we have choose to write our own event listener. In this case React cannot batch the updates, so setState will change the state immediately.
setState((prevState, nextProps) => ({})
variation to overcome state update issues.
State Updates are Merged (From React Docs)
When you call setState()
, React merges the object you provide into the current state.
For example, your state may contain several independent variables:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
Then you can update them independently with separate setState()
calls:
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
The merging is shallow, so this.setState({comments}) leaves this.state.posts intact, but completely replaces this.state.comments.