As you’ve learned the fundamentals of React, you’ve seen how to use functions to create components to develop the user interface for a frontend application. Unsurprisingly, as with most things related to software development, there’s more than one way to create components in React.
When you finish this article, you should be able to:
Up to this point, you’ve written components using functions:
// ./src/Message.js
import React from 'react';
const Message = (props) => {
return (
<div>{props.text}</div>
);
};
export default Message;
But React also allows you to create components using ES2015 classes. Here’s the above function component rewritten as a class component:
// ./src/Message.js
import React from 'react';
class Message extends React.Component {
render() {
return (
<div>{this.props.text}</div>
);
}
}
export default Message;
Every class component must extend (i.e. inherit) from React.Component
and have a render
method that returns the element(s) to render for the component.
Class components are used just like function components:
// ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Message from './Message';
ReactDOM.render(
<React.StrictMode>
<Message text='Hello world!' />
</React.StrictMode>,
document.getElementById('root')
);
From just the above JSX code in the index.js
file, you can’t tell if the <Message>
component is written as a function or class component. Which approach that’s used is an internal implementation detail of the component. In fact, you can switch back and forth between the two approaches provided that you aren’t using a feature that’s only available in class components (more about this in just a bit).
Notice from this example of using the Message
class component that props are set on class components just like you do with function components:
To access props within a class component, use the this.props
property:
If your class component defines a constructor
method that needs access to props, define a props
parameter:
class Message extends React.Component {
constructor(props) {
super(props);
// TODO Initialize state, etc.
}
render() {
return (
<div>{this.props.text}</div>
);
}
}
Just be sure to call the super
method and pass in the props
parameter! Failing to do so will result in the following error:
ReferenceError: Must call super constructor in derived class before accessing
'this' or returning from derived constructor
Remember, a component, whether it’s a function or class component, should never change its own props.
In the early days of React, before using ES2015 classes was commonplace, components were usually created using the React.createclass
function. You’ll still sometimes see this syntax in the wild. If in the future you ever find yourself needing to create a component using the React.createclass
function when targeting newer versions of React, you’ll need to install a separate React module named create-react-class
.
So far, the above example class component behaves exactly as its function component counterpart. This might leave you wondering why you’d want or need to create a class component.
One of the two reasons why you would use a Class component over a Function component is to add and manage local or internal state to your component. The second main reason to use a Class component is to use a component’s lifecycle methods. The following sections will focus on how to add and manage a component’s state. You’ll learn more about a component’s lifecycle (and the associated component lifecycle methods) later in a future reading.
In contrast to props which are provided by the consumer or caller of the component, state is data that’s internal to a component. State is owned by the component where it’s defined and used. That’s why we say it’s “internal” or “local” to that component. Whereas props are not to be changed by a component, state is intended to be updated or mutated (you’ll see how to update state in just a bit). Together, props and state represent the data that’s used to determine how the component should behave and render.
State should only be used when it’s absolutely necessary. If a bit of data is never going to change or if it is needed across the entire application, use props instead.
When you’re just learning React, it can be challenging to know when it’s okay to use state and when it’s not. State is often used when creating components that retrieve data from APIs or render forms. You’ll see examples of those kinds of components later in this lesson. To start, we’ll look at simple, contrived example of using state in just a bit.
Function components are the simplest way to declare a component. If a component doesn’t need to use state or lifecycle methods, it should be written as a function component. A new feature in React, hooks, levels the playing field between function and class components, so that everything you can do in a class component can now be done with function components. You’ll learn about React hooks in a future lesson.
When creating a stateful class component, you can use a class constructor
method to initialize the this.state
object.
Here’s a RandomQuote
component that initializes two state properties, this.state.quotes
and this.state.currentQuoteIndex
, within its constructor
method:
// ./src/RandomQuote.js
import React from 'react';
class RandomQuote extends React.Component {
constructor() {
super();
const quotes = [
'May the Force be with you.',
'There\'s no place like home.',
'I\'m the king of the world!',
'My mama always said life was like a box of chocolates.',
'I\'ll be back.',
];
this.state = {
quotes,
currentQuoteIndex: this.getRandomInt(quotes.length),
};
}
getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
render() {
return (
<div>
<h2>Random Quote</h2>
<p>{this.state.quotes[this.state.currentQuoteIndex]}</p>
</div>
);
}
}
export default RandomQuote;
Notice in the render
method, that the state properties are being accessed using this.state.quotes
and this.state.currentQuoteIndex
.
If you’re following along, be sure to update your application’s entry point to import and render the RandomQuote
component:
// ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import RandomQuote from './RandomQuote';
ReactDOM.render(
<React.StrictMode>
<RandomQuote />
</React.StrictMode>,
document.getElementById('root')
);
If you run your application (i.e. npm start
) and view it in the browser, you’ll one of the five quotes displayed. Refreshing the page will display a new random quote.
Sometimes the same quote will be displayed more than once in a row. You’ll see in a bit how to fix that bug.
Assuming you have the React Developer Tools installed, open up your browser’s developer tools and view the “Components” tab. On the left, you can select the RandomQuote
component to view its current props and state values on the right.
Remember earlier when we said that state should only be used if the data is going to change? Currently, the current quote index doesn’t change after it’s been initialized in the constructor
method. Using state to track this value would make more sense if there was a way to trigger the component to update the current quote index.
To do this, add a <button>
element just below the <p>
element with the following attributes and content:
<button type='button' onClick={this.changeQuote}>Change Quote</button>
Notice the onClick={this.changeQuote}
bit? This is how you add an event listener for the onclick
event. this.changeQuote
is the event handler method and onClick
is the event to listen for.
When adding event listeners, be sure to camelCase the event name (i.e.
onClick
instead ofonclick
) and pass a reference to the event handler method instead of calling it (i.e.this.changeQuote
instead ofthis.changeQuote()
). You’ll learn more about handling events later in this lesson.
Now add the changeQuote
event handler method to the RandomQuote
class:
changeQuote = () => {
const newIndex = this.getRandomInt(this.state.quotes.length);
this.setState({
currentQuoteIndex: newIndex,
});
}
Did you notice the slightly odd looking class property syntax (i.e.
changeQuote = () => { ... }
) that’s being used to define thechangeQuote
method? Using this experimental syntax for defining a class property in combination with an arrow function ensures that you can reliably use thethis
keyword within the method. You’ll learn more about this coding pattern for defining event handlers later in this lesson.
The changeQuote
event handler calls the this.getRandomInt
method to get a new random integer and then calls the this.setState
method to update the component’s state. The this.setState
method accepts an object literal containing the state properties to update.
After updating the state, React re-renders the component and displays the new quote (provided that the current quote index actually changed). Because calling the this.setState
method triggers a re-render, it should not be called from within the render
method, as that would trigger an infinite loop.
Notice that the object literal passed into the this.setState
method only contains the state property that needs to be updated. The this.setState
method merges state updates into the current state object, so you only need to provide the state properties that need to be updated.
Now, instead of refreshing the page, you can click the “Change Quote” button to display a new random quote!
For your reference, here’s the updated RandomQuote
component:
// ./src/RandomQuote.js
import React from 'react';
class RandomQuote extends React.Component {
constructor() {
super();
const quotes = [
'May the Force be with you.',
'There\'s no place like home.',
'I\'m the king of the world!',
'My mama always said life was like a box of chocolates.',
'I\'ll be back.',
];
this.state = {
quotes,
currentQuoteIndex: this.getRandomInt(quotes.length),
};
}
changeQuote = () => {
const newIndex = this.getRandomInt(this.state.quotes.length);
this.setState({
currentQuoteIndex: newIndex,
});
}
getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
render() {
return (
<div>
<h2>Random Quote</h2>
<p>{this.state.quotes[this.state.currentQuoteIndex]}</p>
<button type='button' onClick={this.changeQuote}>Change Quote</button>
</div>
);
}
}
export default RandomQuote;
You should always use the this.setState
method to update state rather than setting the this.state
property directly:
changeQuote = () => {
const newIndex = this.getRandomInt(this.state.quotes.length);
// Don't set the `this.state` property directly
// anywhere outside of the `constructor` method!
// this.state = {
// currentQuoteIndex: newIndex,
// };
// Always use the `this.setState` method to update state.
this.setState({
currentQuoteIndex: newIndex,
});
}
Reassigning this.state
alone won’t trigger re-rendering, leaving your component out of sync.
If you used Create React App to create your application and your application is currently running (using the npm start
command), you’ll receive a warning in the terminal if you reassign this.state
outside of the constructor:
When testing the RandomQuote
component, you might have noticed that sometimes the same quote will display more than once. This is occurring because the this.getRandomInt
method is returning a random integer that’s the same as the current quote index value. We can fix this bug by calling the this.getRandomInt
method until we get a random integer that’s different from the current quote index value.
On the surface, this appears to be simple fix–just use a loop to call the this.getRandomInt
method until you get a random integer that’s different from the current quote index:
changeQuote = () => {
const { quotes, currentQuoteIndex } = this.state;
let newIndex = -1;
do {
newIndex = this.getRandomInt(quotes.length);
} while (newIndex === currentQuoteIndex);
this.setState({
currentQuoteIndex: newIndex,
});
}
The problem with the above solution is that it doesn’t take into account that state updates are handled asynchronously by React. When the currentQuoteIndex
value is retrieved from state (at the start of the method block), you’re not guaranteed that it’s the latest value. There could be a state update that hasn’t been applied yet.
To safely update state based upon the previous state, pass an anonymous method to the this.setState
method (instead of an object literal) that defines two parameters, state
and props
, and returns an object literal containing the state properties to update. The state
and props
parameters give you safe, predictable access to the previous state and prop values:
changeQuote = () => {
this.setState((state, props) => {
const { quotes, currentQuoteIndex } = state;
let newIndex = -1;
do {
newIndex = this.getRandomInt(quotes.length);
} while (newIndex === currentQuoteIndex);
return {
currentQuoteIndex: newIndex,
};
});
}
Now, if you retest your application, clicking the “Change Quote” button will display a different random quote every time!
Currently, the list of quotes doesn’t change once it’s initialized in the constructor
method:
constructor() {
super();
const quotes = [
'May the Force be with you.',
'There\'s no place like home.',
'I\'m the king of the world!',
'My mama always said life was like a box of chocolates.',
'I\'ll be back.',
];
this.state = {
quotes,
currentQuoteIndex: this.getRandomInt(quotes.length),
};
}
Given this, it makes more sense for the quotes to be a prop value. Changing the quotes to a prop would also give the consumer or caller of the RandomQuote
component the ability to customize the list of quotes.
Here’s an updated version of the RandomQuote
component that defines the list of quotes as a prop:
// ./src/RandomQuote.js
import React from 'react';
class RandomQuote extends React.Component {
constructor(props) {
super(props);
this.state = {
currentQuoteIndex: this.getRandomInt(props.quotes.length),
};
}
changeQuote = () => {
this.setState((state, props) => {
const { currentQuoteIndex } = state;
const { quotes } = props;
let newIndex = -1;
do {
newIndex = this.getRandomInt(quotes.length);
} while (newIndex === currentQuoteIndex);
return {
currentQuoteIndex: newIndex,
};
});
}
getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
render() {
return (
<div>
<h2>Random Quote</h2>
<p>{this.props.quotes[this.state.currentQuoteIndex]}</p>
<button type='button' onClick={this.changeQuote}>Change Quote</button>
</div>
);
}
}
export default RandomQuote;
Notice that the constructor
method now defines a props
parameter and passes that parameter into the super
method call:
constructor(props) {
super(props);
this.state = {
currentQuoteIndex: this.getRandomInt(props.quotes.length),
};
}
The changeQuote
and render
methods were also updated to reference the quotes using this.props.quotes
instead of this.state.quotes
.
Now, in the index.js
file, the quotes to randomly display can be passed into the RandomQuote
component:
// ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import RandomQuote from './RandomQuote';
const quotes = [
'Toto, I\'ve a feeling we\'re not in Kansas anymore.',
'Here\'s looking at you, kid.',
'There\'s no crying in baseball!',
'Elementary, my dear Watson.',
'Rosebud.',
];
ReactDOM.render(
<React.StrictMode>
<RandomQuote quotes={quotes} />
</React.StrictMode>,
document.getElementById('root')
);
Unfortunately, now the consumer or caller of the component must set the quotes
prop or the component will throw an error. You can retain the previous behavior by defining a default value for the quotes
prop using the defaultProps
static property:
import React from 'react';
class RandomQuote extends React.Component {
// Code removed for brevity.
}
RandomQuote.defaultProps = {
quotes: [
'May the Force be with you.',
'There\'s no place like home.',
'I\'m the king of the world!',
'My mama always said life was like a box of chocolates.',
'I\'ll be back.',
],
};
export default RandomQuote;
The default quotes
prop value will be used if the consumer or caller of the RandomQuote
component doesn’t provide a value for the quotes
prop.
Now the RandomQuote
component can be used without having to provide the quotes
prop:
// ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import RandomQuote from './RandomQuote';
ReactDOM.render(
<React.StrictMode>
<RandomQuote />
</React.StrictMode>,
document.getElementById('root')
);
In this article, you learned how to: