Functional React Components: A primer, part 1

by
Tags: , ,
Category:

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:

  1. The useEffect function executes before the initial component render
  2. We kick off an interval timer in the browser
  3. Each time the interval hits (every 1 second or so), we call setCounter which was defined in the useState hook to change the counter value
  4. 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.