Tech BlogBrand Name

Writing Stateful Function Components in React Using Hooks

With all the buzz going around, you may already have a good grasp of what hooks are and what they have to offer, but just in case, let's do a quick overview!

Hooks are functions that give function components access to features that used to be exclusive to class components. Local state and lifecycle methods are easily implemented with useState and useEffect. For all our context needs there's — unsurprisingly — useContext. And for more complex operations we have hooks such as useCallback, useMemo and useReducer, as well as the option to create our own custom hooks.

In this post we're going to take a closer look at useState and useEffectand see how we can incorporate local state and effects commonly seen in lifecycle functions, such as fetching and subscribing, into our function components.

useState

The State Hook allows us to easily create stateful function components. The hook takes an argument, which sets our initial state.

The State Hook allows us to easily create stateful function components. The hook takes an argument, which sets our initial state. It also returns an array of two values: the current state and a function that we can use to update it. Through array destructuring, we can set the names of these values ourselves:

const [numberOfKittens, setNumberOfKittens] = useState(5)

setNumberOfKittens replaces our old friend this.setState and is the function we'll use to update numberOfKittens. It is worth noting here that the function we get from our hook will not merge the new and old state together — instead the new state will completely replace the old.

Below is an example of how we might use and update state in a class component. We have an initial state containing an array of products. These products can then be updated using this.setState:

import React, { Component } from "react";

class MagicShop extends Component {
  constructor(props) {
    super(props);

    this.state = {
      products: [
        { id: 1, name: "Haunted Mirror", inventory: 1, price: 5.00 },
        {
          id: 2,
          name: "Mastering Magic: Madame Maude's Meanderings",
          inventory: 372,
          price: 17.50
        },
        { id: 3, name: "Black Cat Shampoo", inventory: 127, price: 2.75 }
      ]
    };
  }

  updateInventory = id => {
    this.setState(prevState => ({
      products: prevState.products.map(product =>
        product.id === id
          ? { ...product, inventory: (product.inventory - 1) }
          : product
      )
    }));
  };

  render() {
    return (
      <main>
        <h1>Laramie's Magical Emporium</h1>

        <section>
          <h2>Goods for Sale</h2>
          {this.state.products.map(product => (
            <article key={product.id}>
              <h3>{product.name}</h3>
              Price: {product.price} € <br />
              {product.inventory} in stock <br />
              <button onClick={() => this.updateInventory(product.id)}>
                Buy
              </button>
            </article>
          ))}
        </section>
      </main>
    );
  }
}

export default MagicShop;

If we re-write this as a function component, we will first add our state using useState. We can then set the initial state by passing it as an argument to useState. To update a product, we use the provided setProducts function.

import React, { useState } from "react";

const MagicShop = () => {
  const [products, setProducts] = useState([
    { id: 1, name: "Haunted Mirror", inventory: 1, price: 5.00 },
    {
      id: 2,
      name: "Mastering Magic: Madame Maude's Meanderings",
      inventory: 372,
      price: 17.50
    },
    { id: 3, name: "Black Cat Shampoo", inventory: 127, price: 2.75 }
  ]);

  const updateInventory = id => {
    setProducts(
      products.map(product =>
        product.id === id
          ? { ...product, inventory: (product.inventory - 1) }
          : product
      )
    );
  };

  return (
    <main>
      <h1>Laramie's Magical Emporium</h1>

      <section>
        <h2>Goods for Sale</h2>
        {products.map(product => (
          <article key={product.id}>
            <h3>{product.name}</h3>
            Price: {product.price} € <br />
            {product.inventory} in stock <br />
            <button onClick={() => updateInventory(product.id)}>Buy</button>
          </article>
        ))}
      </section>
    </main>
  );
};

export default MagicShop;

If we need to keep track of more state, we can just use multiple state hooks:

const [isFoggy, setIsFoggy] = useState(true);
const [isFullMoon, setIsFullMoon] = useState(false);

useEffect

Where class components have lifecycle methods, function components have the Effect Hook. This hook lets you include side effects such as data fetching, subscriptions and updates to the DOM.

In a class component, effects are organised by when they occur during the lifecycle of the component. Hooks instead let us group effects by relation. By default, effects will run after every render, in the order they are specified.

The example below shows how we might fetch data in a class component. When the component has mounted, we fetch a list of all our products and whenever this.state.query is updated we fetch a new list using the user-provided query.

import React, { Component } from "react";

class MagicShop extends Component {
  constructor(props) {
    super(props);

    this.state = {
      products: [],
      query: ""
    };
  }

  componentDidMount() {
    fetch(
      `https://www.laramiesmagicalemporium.com/api/v1/products`
    )
      .then(response => response.json())
      .then(data => this.setState({ products: data.results }));
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.query !== this.state.query) {
      fetch(
        `https://www.laramiesmagicalemporium.com/api/v1/products${
          this.state.query ? "?query=" + this.state.query : ""
        }`
      )
        .then(response => response.json())
        .then(data => this.setState({ products: data.results }));
    }
  }

  handleChange = event => this.setState({ query: event.target.value });

  render() {
    return (
      <main>
        <h1>Laramie's Magical Emporium</h1>

        <section>
          <h2>Goods for Sale</h2>

          <label htmlFor="product-search-query">Search</label>
          <input
            id="product-search-query"
            onChange={this.handleChange}
            value={this.state.query}
          />

          {this.state.products.map(product => (
            <article key={product.id}>
              <h3>{product.name}</h3>
              Price: {product.price} € <br />
              {product.inventory} in stock
            </article>
          ))}
        </section>
      </main>
    );
  }
}

export default MagicShop;

And here is how we could re-write that component as a function component using hooks. Our Effect Hook contains a function that fetches our products and puts them into state using setProducts. The effect runs once after the first render and, because we passed [query] as a second argument to useEffect, it will also run every time the value of query is changed.

import React, { useEffect, useState } from "react";

const MagicShop = () => {
  const [products, setProducts] = useState([]);
  const [query, setQuery] = useState("");

  useEffect(() => {
    const fetchData = async () => {
      const data = await fetch(
        `https://www.laramiesmagicalemporium.com/api/v1/products${
          query ? "?query=" + query : ""
        }`
      ).then(response => response.json());

      setProducts(data.results);
    };

    fetchData();
  }, [query]);

  const handleChange = event => setQuery(event.target.value);

  return (
    <main>
      <h1>Laramie's Magical Emporium</h1>

      <section>
        <h2>Goods for Sale</h2>

        <label htmlFor="product-search-query">Search</label>
        <input
          id="product-search-query"
          onChange={handleChange}
          value={query}
        />

        {products.map(product => (
          <article key={product.id}>
            <h3>{product.name}</h3>
            Price: {product.price} € <br />
            {product.inventory} in stock
          </article>
        ))}
      </section>
    </main>
  );
};

export default MagicShop;

Passing a second argument to useEffect lets us control when the effect is applied. If an effect depends on any values from props or state, we'll want to include all of them in the argument to make sure those values don't go stale.

useEffect(() => {
  setSum(a + b);
}, [a, b]);

If we pass in an empty array ([]), the effect will run only once; meaning props and state inside the effect won't synchronise with outside changes and will keep their initial values. If we leave out the second argument completely, the effect will synchronise with all outside changes and run after every render.

Clean-up

Effects may also return a clean-up function. It runs not only when the component unmounts, but also before every new render where the effect is set to run. Thus the effect from the previous render is cleaned up before the effect is allowed to run again.

Let's look at how we'd add an effect with a clean-up to a class component. If we wanted to subscribe to some outside data source, we would place the closely related subscribe and unsubscribe events in componentDidMount and componentWillUnmount respectively:

componentDidMount() {  
  inventory.subscribeToProducts();
}

componentWillUnmount() {
  inventory.unsubscribeFromProducts();
}

With useEffect, the effect and its clean-up function are kept together in one single function, allowing us to group the events by relation rather than by their place in the component's lifecycle:

useEffect(() => {
  inventory.subscribeToProducts();

  return () => {
    inventory.unsubscribeFromProducts();
  };
});

And there you have it! There is of course loads more to learn about hooks, but hopefully this post will get you off to a good start. Safe travels!