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:
- Removing the need for javascript classes and simplifying components
- 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
- The component renders
- The component calls the callback passed to
useEffect
- The user clicks
Add
to update the state - The component rerenders
- The component calls the cleanup method returned by the
useEffect
callback - 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