Functional -vs- Class Components in React
As I do more blog articles on next.js
and other advanced React APIs, I feel it's important to take a step back and look at Functional React components. They are taking over the React ecosystem, so a good basic introduction will be helpful.
Part 1: The fundamentals
When React was created, its components were defined using JavaScript classes components that inherited from its Component
base class.
import React, { Component } from 'react'; export default class SimplestClass extends Component { render() { return <p>Hello, Class World!</p>; } componentDidMount() { console.log('Component mounted.'); } }
Each Component
has a render
method, and lifecycle methods such as componentDidMount
and componentDidUpdate
.
Along Came Functional Components
Functional components were initially created to provide stateless content. The idea was that as long as you passed your data in as props only to the component, and it didn't use state, the component could be represented as just a render function:
const SayHelloFunctional = ({ name }) => { return <p>Hello, Functional {name}!</p>; }; export default SayHelloFunctional;
In use:
<SayHelloFunctional name="ken"></SayHelloFunctional>
If the name changed (via a bound variable from the outside component) the function would re-run and update the content based on the changed prop.
Components and State
Class-based components provide two main attributes, props
for data provided from the outside, and state
for data owned and modifiable from the component itself.
import { Component } from "react"; export default class DemoFormClassBased extends Component { state = { person: { firstName: "", lastName: "", dateOfBirth: new Date() } }; ... }
State can be attached to forms…
<form onSubmit={this.handleSubmit}> <input type="text" name="firstName" value={this.state.person.firstName} onChange={this.handleChange} /> <input type="text" name="lastName" value={this.state.person.lastName} onChange={this.handleChange} /> <input type="datetime-local" name="dateOfBirth" step={1} value={this.state.person.dateOfBirth} onChange={this.handleChange} /> <button>Submit</button> </form>
… which are handled by form change events and submit actions…
Note: the step
prop provides the granularity of the increment in the input field. We’ve set it to 1 second. Thanks Matt Swartley for catching that…
handleSubmit = (event) => { alert(JSON.stringify(this.state)); event.preventDefault(); }; handleChange = (e) => { const fieldName = e.target.name; const fieldValue = e.target.value; this.setState((previousState) => { return { person: { ...previousState.person, [fieldName]: fieldValue } }; }); e.preventDefault(); };
React did not support state in functional components until version 16.8, when it introduced a new feature named hooks.
Hooks – introducing state and behavior to functional components
With React 16.8's hooks feature, functional components can hold state, handle complex events, and even have their own functional context, eliminiating the need for complex state engines like Redux.
Let's look at how we manage state in a React functional component. Here's a simple timed counter component in class-based React:
import { Component } from "react"; export default class CounterClassBased extends Component { state = { counter: 0 }; componentDidMount() { setTimeout(() => { setInterval(() => { this.setState({ counter: this.state.counter + 1 }); }, 2000); }); } render() { return <p>Counter (Class-based): {this.state.counter}!</p>; } }
The same component using Hooks needs to handle the componentDidMount
callback, and manage its own state.
Managing State with the setState
hook
To manage state, we use the useState
hook for each state variable we want to track. We call this function in the body of the functional component:
import { useState } from 'react'; const CounterFunctional = (props) => { let [counter, setCounter] = useState(0); ... };
As you can see in any basic Hooks demo, the useState
function provides two variables in an array – the first element is the state variable to reference in the function, and the second variable is the method to use to change the state. Calling the setState
method forces an update of the component's state and re-renders the component.
Effects – managing changes from the outside
Our timer component uses the setInterval
javascript function to repeatedly increment the state. In order to set this up, we need the equivalent of a componentDidMount
or componentDidUpdate
method. That equivalent is the useEffect
function.
import { useState, useEffect } from 'react'; const CounterFunctional = (props) => { let [counter, setCounter] = useState(0); // calls the arrow function before the initial render useEffect(() => { setInterval(() => { setCounter(counter++); }, 1000); }, []); };
This seems a bit complex at first, especially the array at the end of the useEffect function. Let's step through what happens:
- The
useEffect
function executes before the initial component render - We kick off an interval timer in the browser
- Each time the interval hits (every 1 second or so), we call
setCounter
which was defined in theuseState
hook to change the counter value - Once the effect is executed the component renders
Every time the counter value changes, it forces a re-render of the component (as the counter was defined using the useState
hook).
The array at the end of the useEffect
function defines the reasons for re-running the effect. These reasons are the names of any other variables that might change. In this scenario we never want to re-run the effect, so we leave the array as empty.
The approach here feels a bit artificial for some actions and a beginner React developer might find hooks confusing at first. But for larger codebases it tends to simplify the code and make it less error prone, since there are less overall moving parts than a class-based approach.
Using Hooks to provide a functional form component
Remember that form we talked about in the beginning? Let's see how we'd set it up in functional React code.
First, we set up the functional form component skeleton:
import { useState } from "react"; const DemoFormFunctional = (props) => { const [person, setPerson] = useState({ firstName: "", lastName: "", dateOfBirth: new Date().toISOString() }); ... };
So far, it's pretty simple – we set up our form data as a single object, person
, which can be modified using setPerson
.
The rendering portion of the function is pretty simple, albeit now we're going to embed the event handlers into the function:
return ( <div> <h1>Demo Form Functional</h1> <form onSubmit={handleSubmit}> <input type="text" name="firstName" value={person.firstName} onChange={handleChange} /> <input type="text" name="lastName" value={person.lastName} onChange={handleChange} /> <input type="datetime-local" name="dateOfBirth" value={person.dateOfBirth} step={1} onChange={handleChange} /> <button>Submit</button> </form> </div> ); };
The event handlers go before the return statement:
function handleSubmit(event) { alert(JSON.stringify(person)); event.preventDefault(); } function handleChange(e) { const fieldName = e.target.name; const fieldValue = e.target.value; setPerson({ ...person, [fieldName]: fieldValue }); e.preventDefault(); }
No major difference, except that we call setPerson
instead of using setState
, and we don't have to use the advanced form or setState
either, because we have the existing person in the function as the person
variable to begin with.
Wrap-up
In some ways, functional components can make reading React code difficult. To really master functional React you have to dive into the useState
, useEffect
, useRef
and other hooks. In the next article I'll go into the finer details, such as how we can store a reference to a timer for cancelation later (on unloading a component for example), and how we can reduce our need for tools like Redux with other hooks such as useReducer
.