In the previous post, we created our first React component. In this post, we'll build on that and explore the issue of state in React, embedding components into components as well as React's component lifecycle.
When we left off, we had explored React's concept of props. Props are constants that we can send to a react component at its initialization time and they remain with the component.
Properties do not change. If we want to create a component with a new property, we would create a new component.
Default Properties
Getting back to our Temperature
component, we have to manually set
our temperature property. If we we to fail to set that property, our
web page would break, possibly not showing anything at all, because
the props.fahrenheit
value would be undefined.
We can set a default property on our component in case a property isn't sent to it.
For that, we'll create a getDefaultProps
method, which will return a
Javascript object containing default properties. In our example, we'll
set a default temperature of 72F.
To do that, create a new method for our Temperature
component:
getDefaultProps: function(){
return {
"fahrenheit": 72
}
},
Now we can either set the temperature explicitly as we did before, or we can omit the temperature property and a default value will be set for us.
Getting dynamic with state
So far we've been using a static temperature that we've either been setting manually or using a default value for. Now we're going to create a component that is more dynamic.
Let's imagine that we have an RESTful API /temperature
which returns
a JSON response in the form of {"fahrenheit": 80}
. Instead of
setting our component manually, we could use this API endpoint
to set our temperature.
While we could use props for this, if we only intend to set the
temperature once, we're going to go ahead and use state
instead.
React components' state
property works similarly to the props
property except that values in state
are changeable using the
setState
method, which we'll use. It's important to always use
setState
to set the values inside state, rather than trying to set
them directly.
We'll want to fetch the current temperature as soon as we can. The
best time to do this is right before the component will mount to the
DOM. So we'll set the React method componentWillMount
to retrieve
our value and set the temperature. We'll also change our render method
to use the state, instead of the prop.
var Temperature = React.createClass({
componentWillMount: function(){
request.get('/temperature', function(res){
this.setState({fahrenheit, res.body.fahrenheit})
}.bind(this));
},
render: function(){
return (
<h1>The temperature is {this.state.fahrenheit}F<h1>
)
});
What's Happening Here
Although we made a small change, there's quite a bit to go over.
Out componentWillMount
method will fire just before the component is
rendered in the browser, and it will send off a RESTful call to the
URL /temperature
.
Once that's complete, it will fire off the anonymous function that
we've passed request.get
, which calls the this.setState
method
with the newtemperature
property.
this
returns
You may notice that we've added .bind(this)
to our anonymous function
call to request.get
. That's because we call the this.setState
method on it, and this
is always set to the current function
namespace. We can get around that by binding this
, our component, to
the anonymous function- thereby giving it access to all the properties
of our component, including the setState
method.
Look Ma, no render()
The next thing you may notice is that we didn't explicitly call
render()
after we set our new state. That's because React will do
that for us. Once we set a new state, the render
method will get
re-run and, if necessary, the page will be refreshed. More on this
later in the post.
If you've had to work directly with DOM manipulation, then you'll see the clear difference here. We only need to worry about the state of our component through properties and state, and the component will take care of its own display.
There's still a problem
If you run this code, the render()
method will be executed while the
request.get
is still waiting. That's the asynchronous nature of
Javascript, and it's generally a good thing.
The problem for us is that in the meantime, our render
method will
return something like:
The temperature is undefined
, or worse, may fail to run at all.
There are two ways we can solve this. The first is that we can set a default temperature, and the second is that we can put some logic in our render method to handle this condition. Let's go through both.
Setting Default State
To start, let's just set a default temperature just like we did in the
previous post. For that, we'll use the getInitialState
method.
Add the following method to our component:
getInitialState: function(){
return {
fahrenheit: 72
}
},
When the component runs, for a brief period, it will display the temperature as 72 until the request call completes.
Using logic during render
Instead of setting a fake temperature value, we can add some logic to our render method telling the user that we're fetching the temperature.
Let's do that using the original component as displayed early on in the post:
var Temperature = React.createClass({
componentWillMount: function(){
request.get('/temperature', function(res){
this.setState({fahrenheit, res.body.fahrenheit})
}.bind(this));
},
render: function(){
if (this.state.fahrenheit === undefined){
return (<h1>Getting temperature...</h1>)
} else {
return (
<h1>The temperature is {this.state.fahrenheit}F<h1>
)
}
});
Now while the request is in process, the message Getting temperature
will be displayed to the user.
Combining default state with render logic
It's nice to be able to set a message that the temperature isn't yet
set, but using undefined is probably not a good idea. Instead, let's
set the state of fahrenheit
to null. Because null is distinct from
undefined, we can test for it explicitly.
So let's do that:
var Temperature = React.createClass({
getInitialState: function(){
return {
fahrenheit: null
}
},
componentWillMount: function(){
request.get('/temperature', function(res){
this.setState({fahrenheit, res.body.fahrenheit})
}.bind(this));
},
render: function(){
if (this.state.fahrenheit === null){
return (<h1>Getting temperature...</h1>)
} else {
return (
<h1>The temperature is {this.state.fahrenheit}F<h1>
)
}
});
Flexibility
We're using the url /temperature
for our RESTful call, but that URL
could be anything. In fact, we could be setting the state from any
function call at all. That's in contrast to some other libraries which
dictate exactly how the server needs to lay out its data. Here, we
have the ultimate control over that and we can choose an API that
makes sense for us.
Multiple components
So far we've only created a single component, but we can create multiple components.
We're going to create one component that displays the temperature in Fahrenheit and one that displays it in Celsius.
var Fahrenheit = React.createClass({
render: function(){
return (
<h1>{this.props.fahrenheit}F</h1>
)
}
});
var Celsius = React.createClass({
render: function(){
var celsius = (this.props.fahrenheit - 32) * 5 / 9;
return (
<h1>{celsius.toPrecision(1)}C</h1>`It
)
}
});
var Temperature = React.createClass({
componentWillMount: function(){
request.get('/temperature', function(res){
this.setState({fahrenheit, res.body.fahrenheit})
}.bind(this));
},
render: function(){
return (
<div>
<Fahrenheit fahrenheit={this.state.fahrenheit} />
<Celsius fahrenheit={this.state.fahrenheit} />
</div>
)
}
});
Reusable Components
Instead of creating the rendered output directly in the Temperature Component, we now use the Fahrenheit and Celsius components. Using JSX we can treat these components just like regular HTML in our application. This level of abstraction is very useful as we build larger and more complex applications.
In addition to the ease of creation, we do not need to worry about our children object's state. That is handled for us in our Render method. That means that at each component level, we can focus our attention exclusively on that component without worrying about what either the parent component or the child component are doing.
Using Props
You may notice that we're using props for our temperature display, rather than state.
That's because we know that the temperature for each of these components is not going to change, so props make sense. The props for the components does not change, instead, new components are generated.
Making our code more clear
Having the data retrieval in componentWillMount
works, but it's not
entirely clear initially what's going on. Let's make it more clear by
adding a new method to Temperature
that explicitly says that we're
fetching the temperature:
getTemperature: function(){
request.get('/temperature', function(res){
this.setState({fahrenheit, res.body.fahrenheit})
}.bind(this));
},
Now we'll reference this method in our componentWillMount
, so it
will become:
componentWillMount: function(){
this.getTemperature()
},
Custom Methods
Writing our component this way has an added benefit of more
readability. It's more obvious what's going on now inside of
componentWillMount
.
Because React components are normal Javascript objects, we can add new methods to them. The React framework gives us guidelines but generally stays out of the way.
Getting Updates to our Temperature
What if we wanted to get regular updates to the temperature? We can
use Javascript builtin setInterval
to regularly retrieve the
temperature from our API endpoint and update the temperature
accordingly. Now that we have getTemperature
as its own function, we
can do that pretty easily.
We'll change our componentWillMount
method to add the setInterval
call. setInterval
returns an identifier to keep track of the timer-
so we'll store that timer in a state variable in case we want to use
it later.
componentWillMount: function(){
var intervalID = window.setInterval(this.getTemperature, 30000);
this.setState({intervalID: intervalID});
}
So now the component will re-load the temperature from the server every 30 seconds.
The Shadow DOM
You might think that destroying an old component and creating a new one would be slow, but React optimizes this by creating a copy of the DOM in memory and then only editing that DOM. It then create a diff of the two and only modifies on screen what needs to be changed. This is done incredibly quickly, so screen refresh times are very short, in the tens of milliseconds.
Cleaning up after ourselves
In our case, we do not destroy the Temperature component without also
closing the window, but it's possible for us to do so in another
setting. If we were to do that, the browser would still make the
getTemperature call every 30 seconds, because that function is bound
to the window
object, not just the Temperature
instance.
To address this, let's add a new method to our Temperature
component
that removes the interval call upon component destruction. The method
name for this in react is componentWillUnmount
:
componentWillUnmount: function(){
window.clearInterval(this.state.intervalID);
},
Now we're free to create and destroy as many Temperature
component
instances as we like.
Summing it up so far
In this post we covered using React state
, we covered making
components that have children components, integrating React components
with external data, as well as using the React lifecycle methods to
create methods that execute before and after the component mounts.
In the next post, we'll go through how React makes forms easier, as well as touch on using CSS frameworks to help us build our applications more easily.