Displaying the time
Let's add an element to our App component that displays the time. ⏰ To do this, we'll declare a variable and reference it in the component markup.
Back in App.js, declare a variable time
and then display it inside a new <p>
element at the bottom of the component:
let time = new Date().toLocaleString();
class App extends Component {
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
Hi {this.props.name}!
</p>
<p className="App-clock">
The time is {time}.
</p>
</div>
);
}
}
You should see two lines of text in your app now, one with your name and one with the current time when the page was refreshed.
The clock component
Since this is a JavaScript app and not just a static web page, it would be nice if the time updated itself instead of just growing stale on the page like this. In order to do that, we'll need to add some functionality.
This seems like a good time to break out the little clock into its own component!
Breaking the code out into a new component
Let's remove the whole <p>
tag and its contents that we created to display the time, and move it to a new file called src/Clock.js
. Remember that we need the React
object in scope to make JSX work, so let's import it at the top of the page, and define a new component class just as was done for us in the App
component. Let's skip the destructuring in the import statement at the top so we can see more simply what's going on here:
import React from 'react';
class Clock extends React.Component {
}
export default Clock;
As is, this is a complete module. But it won't work because our new component doesn't have a render
method. So let's add that. Now our file should look like:
import React from 'react';
let time = new Date().toLocaleString();
class Clock extends React.Component {
render() {
return (
<p className="App-clock">
The time is {time}.
</p>
);
}
}
export default Clock;
Now in the App
component we have removed the variable declaration and the <p>
tag we created for the clock. All that remains now is to call our new component here:
class App extends Component {
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
Hi {this.props.name}!
</p>
<Clock ></Clock>
</div>
);
}
}
Now if you save all this work and visit the page again, you'll see another helpful error message. The App
component doesn't know about the Clock component's existence yet. If you aren't sure how to remedy this, take a look at how index.js
makes itself "aware" of the App component, and then try doing the same thing here with Clock until you get the app to work again.
Using state
We still haven't solved the problem of making the clock keep up with the time. One approach could be to re-render the component every second. This sounds like it might work, but React wants to manage rendering on its own, and it provides us with an interface for notifying it of changes that may require a re-render. It's called setState
, and it's the only way we should try to make direct changes to the state of the UI.
Instead of declaring a variable at the top of the file, let's make use of a feature of ES Classes called constructor functions. The constructor function is called when a class is instantiated, and it's a good place to put business that should happen during initialization. We'll use the constructor to set the initial state of the clock. Then we'll make use of the state property instead of the variable we had previously in the markup.
import React from 'react';
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {
time: new Date().toLocaleString()
};
}
render() {
return (
<p className="App-clock">
The time is {this.state.time}.
</p>
);
}
}
export default Clock;
super()
is a javascript keyword that is used to call functions on a parent's object.
In this case, it will call the constructor function on the base React.Component
class. This is necessary to preserve the functionality of the constructor function of the class your component is inheriting from. props
is being passed along as well to make sure super
has access to the same arguments as the overwriting constructor.
You should always pass props
when calling the super constructor in a component class.
Check your app page now. It should be exactly the same.
We still haven't made the clock dynamic, but we've set up the groundwork for it.
state
is private
state
is a private property of components, a piece of information that only they can read and only they can change. Nothing outside of a component should ever know about a component's state. If a component wants to share information about its state with another component, it can do that by passing it explicitly via props in the call to the other component.
Making the clock tick with lifecycle hooks
Next we are going to make the clock tick. We'll use a simple JavaScript setInterval
to make an update every second to the time object. The question is, where to do this?
We could do it at the top of the file, but since we're using this.state
to keep track of the time, we won't have access to that there.
We could do it in the constructor function, which would work, but that will start a timer when the Component is instantiated, whereas we want to start our timer when the component's output is inserted into the DOM for the first time. Fortunately React provides us with special lifecycle hooks for the different stages of a component's life in the DOM.
When a component's output is first inserted into the DOM it will trigger a hook called componentDidMount
, and when it's removed from the DOM it will trigger one called componentWillUnmount
. Let's use these to setup our clock when the component is mounted, and to remove our interval when the component is unmounted, so that the interval doesn't hog memory after it's no longer in use. We can insert these right into our class definition:
componentDidMount() {
this.intervalID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.intervalID);
}
We store the interval's id so that we can clear it later when the component is unmounted. Note that we store it directly on this
. We could store it in a variable initialized at the top of the file, but we shouldn't use state
to store this, because it's not being used in render. From the documentation:
If you don't use something in render(), it shouldn't be in the state.
The arrow function is being used here to preserve the scope of this
.
Now we'll add the method that will trigger React to update. Here's where state
is especially useful. If we want React to know about our changes, we don't update state
directly. Instead we'll use setState
, which merges our desired updates into state
and also informs React of the changes. From there, React will keep track of where in the DOM our changes need to be updated.
tick() {
this.setState({
date: new Date().toLocaleString()
});
}
Now our component class should look like this:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {
time: new Date().toLocaleString()
};
}
componentDidMount() {
this.intervalID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.intervalID);
}
tick() {
this.setState({
time: new Date().toLocaleString()
});
}
render() {
return (
<p className="App-clock">
The time is {this.state.time}.
</p>
);
}
}
When you save this file and visit the page, you should see the ticking clock!
Using props
and state
while setting state
Sometimes when using setState
you want to refer to the props
object or to the previous state
. To do this you must use a second form of setState
that accepts a function as an argument, rather than directly passing the state object. This is because the value of state
and props
within a call to set state may be unpredictable, since React may choose to set state asynchronously. Given some arbitrary logic that looks like this:
doTheMultiplication() {
this.setState({
value: this.state.thingToMultiply * this.props.multiplier
});
}
the fix would be to refactor to:
doTheMultiplication() {
this.setState(function (prevState, props) {
return {
value: prevState.thingToMultiply * props.multiplier
};
});
}