Hooked on (React) Hooks
Mar 2, 2020
In February 2019, the React team released version 16.8 which included a feature that has fundamentally changed the way we approach writing components – hooks!
Before hooks came along, managing state or handling lifecycle functionality had to be done using class components. Because of this requirement, you’d often find yourself spending precious time converting functional components to class components. Not anymore! We can now simply use functional components for everything and it’s truly fantastic.
What are hooks?
React hooks are simply functions that allow you to hook into certain, possibly stateful, React functionality. Being functions, they are highly reusable and can be used multiple times while remaining independent of each other. Additionally, they’re 100% backwards compatible making it super easy to give them a try without worrying about breaking your app. That said, keep in mind that hooks will not work inside class components.
While React ships with a number of different hooks (and you can even write your own), there are a few you’ll use more than others, namely useState, useEffect, useRef, and useCallback. Let’s dig into each one:
1. useState()
Without a doubt, this is the hook you’ll use the most because as the name suggests, we use it to manage state. Before we look at an example, here’s a quick summary of what useState offers us:
- State can be any type (recall class-based state had to be an object)
- Values are preserved between re-renders.
- Can be used multiple times to manage multiple states
- Always returns an array with exactly two elements – your current state and a function to update that state.
- When you update state, your previous state is fully replaced. This differs from class-based state that would merge your new and previous state.
- React automatically passes the previous state to your setter function. This is useful when we're updating state that relies on the current value.
Now, let's look at some code. Let's say we're building a counter component and we need state to manage the 'count' value. Here's how that's done with useState:
import React, {useState} from 'react';
function Counter() {
// Initialize 'count' with a value of 0
// Use array destructuring to quickly assign our state and its
// setter function to variables
const [count, setCount] = useState(0)
return (
<p>Current count: {count}</p>
)
}
export default Counter;
// While array destructuring is the common convention, you could
// also do something like this, since we know useState always returns
// an array with exactly two values
const countStateVariable = useState(0)
const count = useState[0]
const setCount = useState[1]
// but why write three lines of code when you can write one?
Easy, right? Just like that we have a state variable called 'count', initialized with a value of 0. Now, let's use our setter function to increment the count by 1:
import React, {useState} from 'react';
function Counter() {
// Initialize 'count' with a value of 0
// Use array destructuring to quickly assign our state and its
// setter function to variables
const [count, setCount] = useState(0)
const increment = () => {
// Recall React automatically passes the previous state value
// We can call it anything but 'prevCount' makes sense here
setCount(prevCount => prevCount + 1)
}
return (
<div>
<p>Current count: {count}</p>
<button onClick={increment}>Add 1</button>
</div>
)
}
export default Counter;
You might be wondering - why can't we write setCount like this:
setCount(count + 1)
Well, you technically can but it's safer to access the previous state through the argument React automatically passes for us. Here's how the React team explains it:
2. useEffect()
While its use isn't as immediately obvious as useState, useEffect is a super helpful hook that is used to manage side effects (i.e. anything that affects something outside of the function being executed). In short, useEffect tells React that your component needs to do something after render. React remembers the function you passed and calls it later after performing the DOM updates. The function we pass is our effect. Some common use cases include data fetching, API requests, timers, event handlers, and subscriptions. Again, before we look at an example, here are some important characteristics of this hook:
- By default, it executes after every render cycle, including the first render.
- Combines componentDidMount, componentDidUpdate, and componentWillUnmount into a single API.
- Passing an empty array as the second argument makes it act like componentDidMount (i.e. only runs after the first render).
- Adding dependencies to the array will tell React to only run the useEffect on the first render and when one of the specified dependencies has changed.
- Can be used multiple times within the same component.
- React defers running useEffect until after the browser has painted, so doing extra work is less of a problem because it won’t block the browser.
Okay, let's add a simple useEffect to the Counter component we wrote above:
import React, {useState, useEffect} from 'react';
function Counter() {
const [count, setCount] = useState(0)
const increment = () => {
setCount(prevCount => prevCount + 1)
}
useEffect(() => {
document.title = `You clicked ${count} times`;
})
return (
<div>
<p>Current count: {count}</p>
<button onClick={increment}>Add 1</button>
</div>
)
}
export default Counter;
Now every time the component renders, the document's title will be updated.
But what if the value of 'count' hasn't changed between renders? Does it really make sense to run the effect again? The simple answer is no - and that's where the dependency array comes in. In order to tell React to skip applying an effect if certain values haven't changed between re-renders, we can supply an array containing those values as a second argument, like this:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count])
Easy Peasy.
*useRef and useCallback coming soon!