Getting Hooked on React Hooks

React Hooks have been available in a stable release since React 16.8 back in February. Hooks address a number of grievances that developers have with React by primarily achieving 2 things:

  1. Removing the need for javascript classes and simplifying components
  2. Allowing users to share stateful logic across multiple components

In this article I would like to demonstrate how the introduction of React Hooks addresses that first task by converting simple React class components into their React Hook equivalents and then bringing it all together with a more complex example

Using useState to create stateful functional components

Here’s a simple React component that uses state to track which div is highlighted.

import React, { Component } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

class App extends Component {
  constructor() {
    super();
    this.state = { selected: null };
  }

  render() {
    const { selected } = this.state;
    return (
      <div className="container">
        <div
          className={`box${selected === 1 ? " selected" : ""}`}
          onClick={() => this.setState({ selected: 1 })}
        />
        <div
          className={`box${selected === 2 ? " selected" : ""}`}
          onClick={() => this.setState({ selected: 2 })}
        />
        <div
          className={`box${selected === 3 ? " selected" : ""}`}
          onClick={() => this.setState({ selected: 3 })}
        />
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Try it here

And here's the exact same component built as a functional component using React Hooks

import React, { useState } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [selected, setSelected] = useState(null);
  return (
    <div className="container">
      <div
        className={`box${selected === 1 ? " selected" : ""}`}
        onClick={() => setSelected(1)}
      />
      <div
        className={`box${selected === 2 ? " selected" : ""}`}
        onClick={() => setSelected(2)}
      />
      <div
        className={`box${selected === 3 ? " selected" : ""}`}
        onClick={() => setSelected(3)}
      />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

You can already begin to see how React Hooks allow us to write more concise components. We initialize each individual state value by calling useState with the initial value. useState returns an array of 2 values that we conventionally access using array destructuring. The first value selected is used to access the access the current state while the second value setSelected is a method used for updating the state. The argument to setSelected can either be the new state value or a callback function that takes the old state as parameter.

To use another state we simple call useState again. Each piece of state is tracked separately.

import React, { useState } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function getRandomColor() {
  var letters = "0123456789ABCDEF";
  var color = "#";
  for (var i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)];
  }
  return color;
}

function App() {
  const [selected, setSelected] = useState(null);
  const [backgroundColor, setBackgroundColor] = useState("white");

  const handleClick = id => {
    setBackgroundColor(getRandomColor());
    setSelected(id);
  };

  return (
    <div className="container" style={{ backgroundColor }}>
      <div
        className={`box${selected === 1 ? " selected" : ""}`}
        onClick={() => handleClick(1)}
      />
      <div
        className={`box${selected === 2 ? " selected" : ""}`}
        onClick={() => handleClick(2)}
      />
      <div
        className={`box${selected === 3 ? " selected" : ""}`}
        onClick={() => handleClick(3)}
      />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Try it here

Using Effects to replace lifecycle methods

Every time the component updates, useEffect is called after render, thereby fulfilling the same role as componentDidUpdate did with class components.

Here's a classic counter component that will log the count value on every update

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => console.log(count));

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count => count + 1)}>Add</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

Try it here

But what if we don't want to run our code every time our components updates? What if we want to create a "Count Logger" by setting a 1 second interval that will log our count?

If we try

useEffect(() => {
  setInterval(() => {
    console.log(count)
  }, 1000)
});

then every time we update the state we will create a new interval. Instead we should try to only set our interval once when our component first mounts.

To mimic componentDidMount we can simply add an empty array as a second argument to useEffect

useEffect(() => {
  setInterval(() => {
    console.log(count)
  }, 1000)
}, []);

The second argument to useEffect is an array of dependency variables (typically state or props) that the component will watch. useEffect will only run following a render if one of those variables has changed. By providing an empty array useEffect will only run one time when the component first mounts.

Now useEffect only runs when the component in first mounted, but now we have a new problem. Try it out.

When we update the state, our logger still logs a count of 0. This happens because Effects actually differ somewhat from React Lifecycle methods.

In React classes, when lifecycle methods access state, that state value is always the most recent state.

Effects on the other hand only access the props and state value from the render they immediately follow.

In our case, useEffect is called immediately after the component is mounted when count = 0. This means that when useEffect is called we're actually calling:

setInterval(() => {
  console.log(0)
}, 1000)

which results in our logger only logging 0 every second.

To fix this we need to do 2 things.

First of all, we cannot merely set up the interval when the component is mounted. Instead we have to set a new interval every time count is updated. As described before this is very simple. We just have to add count to our dependency array.

useEffect(() => {
  setInterval(() => {
    console.log(count)
  }, 1000)
}, [ count ]);

Now we're back to our original problem. Every time we update the state we create a new interval that logs the new state every second. What we need is a way to clear the interval every time the components updates.

useEffect actually gives us a way to clean up our old effects before running useEffect after every render. If the callback provided to useEffect returns a function, then on every render that returned function will be called before the callback.

To clarify, here's a counter component that logs each step of rendering a component with useEffect.

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Run Effect Callback')
    return () => {
      console.log('Run Effect Cleanup')
    }
  });

  const handleClick = () => {
    console.log('Update State')
    setCount(count => count + 1)
  }

  console.log('Render')
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={handleClick}>Add</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

Try it here

Here's the order of events

  1. The component renders
  2. The component calls the callback passed to useEffect
  3. The user clicks Add to update the state
  4. The component rerenders
  5. The component calls the cleanup method returned by the useEffect callback
  6. The component calls the callback passed to useEffect

Returning to our original problem, we can use this returned cleanup method to clear our intervals after every render.

Here's our final Count Logger

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count);
    }, 1000);
    return () => clearInterval(id);
  }, [ count ]);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count => count + 1)}>Add</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

Try it here

Using useRef to reference child components and DOM nodes

The most typical example of using a ref in a class component is creating an uncontrolled form.

import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = null;
  }

  handleSubmit(event) {
    event.preventDefault();
    alert("A name was submitted: " + this.input.value);
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={ref => this.input = ref} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Try it here

React Hooks gives us the useRef method to implement these references in functional components. Here's how the same component could be implemented using Hooks.

import React, { useRef } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const nameInput = useRef(null);

  const handleSubmit = event => {
    alert("A name was submitted: " + nameInput.current.value);
    event.preventDefault();
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input type="text" ref={nameInput} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Try it here

useRef returns a javascript object that stores a mutable value in its .current property. This object can be passed to a ref attribute to create a reference to a DOM element or child component.

useRef can be used for much more that just referencing child nodes however. It can be used for tracking any piece of mutable state. This would imitate how state works in class components.

Be aware however that this approach breaks out of the typical React Hooks paradigm and may look a bit clunky. I personally would not recommend this approach and instead embrace the new approach to state that React Hooks provides.

Going back to our "Counter Logger" component, if we only wanted to set our interval once, we could save our count state as a mutable ref and then update the ref whenever count is updated.

import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(0);

  useEffect(() => {
    setInterval(() => {
      console.log(countRef.current);
    }, 1000);
  }, []);

  useEffect(() => {
    countRef.current = count;
  }, [ count ]);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count => count + 1)}>Add</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

Try it here