The core purpose of a React component is to define the displayed view and bind it to the code that drives its behavior. React’s functional components distill this down to the simplest possible profile: a function that receives properties and returns a JSX definition. Everything required for behavior is defined within the function body, and the class-related parts of object-oriented components are dropped.
Functional components are capable of performing all the work of a class-based component beginning with React 16, via the “hooks” API.
Simple class-based vs. functional comparison
Let’s begin by comparing a very simple class-based component with a functional version.
Listing 1. Simple class-based component
class QuipComponent extends React.Component {
render() {
return <p>What gets us into trouble is not what we don't know. It's what we know for sure that just ain't so.</p>;
}
}
<div id="app"></div>
ReactDOM.render(<QuipComponent />, document.querySelector("#app"));
Listing 2. Simple functional component
function QuipComponent() {
return <p>Outside of a dog, a book is a man’s best friend. Inside of a dog, it’s too dark to read.</p>;
}
<div id="app"></div>
ReactDOM.render(<QuipComponent />, document.querySelector("#app"));
In both cases, the components simply output a paragraph element with content. Notice that the functional version, besides the same call to ReactDOM.render()
has no interaction with any APIs. It is just a normal JavaScript function.
This is a small difference, but it is a difference in favor of the simplicity of functional components. In general, any time you can reduce the API footprint of code, it is beneficial to the overall simplicity of the system.
An advantage of object-oriented code is that objects provide a structural organization to the code. In the case of interface components, that structure is provided by the component system itself. A function provides the simplest way to interact with the component rendering engine.
The functional version, instead of calling ReactDOM.render()
, simply returns a value, which is also JSX.
Props
To accept properties, a functional component accepts an argument, as in Listing 3. Parent components pass the properties in via DOM attributes in the same way as seen in ParentComponents
.
Listing 3. Functional props
function ParentComponent() {
return (
<div>
<h1>A Clever Thought</h1>
<QuipComponent quip="Don't believe everything you think." />
</div>
)
}
function QuipComponent(props) {
return <p>{props.quip}</p>;
}
Note that it is possible to define functional components with default return values, eliminating the return keyword via fat-arrow syntax as seen in Listing 4.
Listing 4. Fat-arrow functional syntax
const QuipComponent = props => ( <p>{props.quip}</p> )
State and hooks
With a class, you use this.state()
and this.setState()
to manage a component’s internal state. In the case of functional components, you use what is known as a hook.
In the case of component state, you use the setState()
hook. At first glance, this syntax may seem odd, but it is actually simpler than the class-style state handling.
Hooks are so called because they allow functions to interact with the React engine; that is, they “hook” into it. Note that you import the non-default useState
function along with React itself (or use React.useState()
).
Listing 5. useState hook
import React, { useState } from 'react';
function QuipComponent(props) {
const [votes, setVotes] = React.useState(0);
const upVote = event => setVotes(votes + 1);
return <div>
<p>{props.quip}</p>
<p>Votes: {votes}</p>
<p><button onClick={upVote}>Up Vote</button></p>
</div>;
}
The form for the useState hook is this: const [variableName, variableModifier] = useState(defaultValue)
. The variable name, in this case votes
, exposes a variable that can be referenced by the template and code as seen here in {votes}
. To update the variable, the variable modifier function is used, in this case setVotes
.
To interact with this state modifier, a button with a standard React event handler has been added. As always with React, state is only modified via the setter, never accessed directly (that is, you don’t write votes++
).
Lifecycle events with useEffect
The fundamental purpose of useEffect is to allow React’s render engine to reach inside component functions and initiate some action, to cause some effect.
There are four basic capabilities offered by useEffect:
- Do something when the component renders
- Do something when the component renders but only the first time
- Do something when a specific variable updates
- Do something when the component unmounts, i.e., clean up
All four of these are achieved via the same syntax: import useEffect, then call it with a function as the first argument. If it returns a function, this function is called when the effect is complete, that is, it cleans up the side effect. If there is a second argument, it is an array specifying which variables to watch to trigger the effect. If the array is empty, then the function is only called upon initial render.
The general form of useEffect is as shown in Listing 6.
Listing 6. General form of useEffect
import React, { useEffect } from “react”;
useEffect(() => {
/* do work */,
/*optional cleanup */ return () => {}
)}, /*optional*/ [/*0-N array members])
This syntax contains everything that class lifecycle hooks gave us without the pitfalls of the *Mount callbacks. Let’s unpack the four capabilities one by one to make our understanding of useEffect more concrete.
Run once on initial render
Let’s say you want to run something just the first time when the component is rendered. In Listing 7, we’re going to start an interval. It could be a subscription to a service, for example.
Listing 7. Start an interval with useEffect
useEffect(() => {
setInterval(function(){
console.info("Another second has slipped into the past.");
// This code contains a flaw! See the cleanup in version Listing 8.
},1000);
}, []);
Listing 7 is not complex, but it contains the most mysterious part of the useEffect animal: the empty array as a second argument. This empty array tells the effect to run it only on the first render. If the second argument were not present at all, then React would call the effect upon every render.
Clean up
As noted in the comment in Listing 7, this effect needs a cleanup, because it uses an interval. Imagine if the user navigates off the component and then returns. The interval could easily be still alive and you could spawn a multitude of them. That is not the behavior we want.
Listing 8. Clean up callback with useEffect
useEffect(() => {
const interval = setInterval(function(){
console.info("Another second has slipped into the past.");
},1000);
return () => {
clearInterval(interval);
}
}, []);
The nice thing about the code seen in Listing 8 is that the cleanup required for the interval inside the effect is contained right there as a natural part of the function itself: It is the return value. The function returned by the effect will be called when the effect completes and therefore can take care of any cleanup — in this case, by disposing of the interval with clearInterval()
.
Thanks to JavaScript’s lexical scoping and the useEffect syntax, we get to define everything in one place: the when, the what, and the cleanup.
Targeting (watching) a variable
Now, there are instances where you want to only perform an effect if a certain value is updated. For example, you want to perform an action whenever a property that came into the functional component is changed. Listing 9 has an sample.
Listing 9. Variable-specific useEffect
const MyComponent = (props) => {
useEffect(() => {
console.info("OK, it was updated: " + props.anImportantVar);
}, [props.anImportantVar]);
}
Listing 9 is in fact a fairly powerful reactive arrangement packed into a small syntax. You can roll up powerful event-based component behavior from it.
You can think of this as a way to hook into the reactive engine and cause additional behavior you require. Combined with function props you can wire up some very clean and powerful inter-component reactive behavior. The same variable watching can be applied to state managed via the useState
hook.
When using the variable watching feature, keep in mind that you need to include all of the variables the function makes use of, otherwise it might operate on stale values.
Functional components and React’s future
Functional components offer a simplicity edge over class-based components, and with hooks they have the same capabilities. Going forward, functional components will become more prominent, since there is no compelling reason to continue using two different syntaxes.
This article has illustrated the essential elements necessary to understanding and using functional components. You have seen the most common hooks, useState
and useEffect
, but there are more, and you can familiarize yourself with them here. You can also define your own hooks as described here.