Academind Logo

React Hooks Crash Course

React Hooks will change the way we write React components. Here's the 'Why' and - most importantly - the 'How'?

Created by Maximilian Schwarzmüller
#

What are React Hooks?

React 16.8 introduced a brand-new feature to React: "React Hooks".

React Hooks (or just "Hooks") allow you to build your entire React app with functional components only. Yes, that's right, no more classes.

State management, lifecycle methods & side effects and many other things can now all be implemented in functional components - with the help of these Hooks.

If you're interested WHY React Hooks were added, you can find the long form explanation here.

In short, Hooks were added to replace classes because:

  • Re-using (stateful) logic between components was difficult in the past

  • classes can be confusing and could easily be used incorrectly (e.g. wrong usage of lifecycle hooks, subtile bugs)

That being said, it is important to point out that classes are not going anywhere!

You can build all components as functional components with React Hooks but neither you have to so, nor you have to convert all your existing, class-based components to functional ones with Hooks.

And whilst it seems likely that React Hooks will become the new standard for building components, it's way too early to tell which role they'll play exactly in the future of React.

With that out of the way, what do Hooks look like? How are they used?

We got two major reasons for using classes in the past:

So let's see, how these things are implemented with hooks.

#

State Management

Prior to React Hooks, you managed state like this:

import React, { Component } from 'react';
class Shop extends Component {
state = { cart: [] } // highlight-line
cartHandler = () => {
this.setState({ cart: ['A Book'] } // highlight-line
}
render() {
return (
<button onClick={this.cartHandler}>
Add to Cart
</button>
)
}
}

And here's how you'd do that with React Hooks (only available in React > 16.8):

import React, { useState } from 'react' // highlight-line
const Shop = props => {
const [cart, setCart] = useState([]) // highlight-line
const cartHandler = () => {
setCart(['A Book']) // highlight-line
}
return <button onClick={cartHandler}>Add to Cart</button>
}

Pretty interesting, huh?

useState() is a so-called Hook.

It's baked into React's core API and you use it to manage state in functional components.

The way it works is like this:

  1. You pass in your initial state ([])

  2. It returns an array with exactly 2 elements ([cart, setCart] => your current state and a state-setting function)

  3. You access the state with the first element and set it with the second element (which is a function)

In case you're not familiar with the array-destructuring syntax I'm using in the example, you can learn more about it here.

#

State is not getting merged!

Besides the different syntax, useState() also works differently than state + setState did in class-based components!

When you set a new state with React Hooks (i.e. via setCart in our example), the old state will always be replaced!

Hence, if you have multiple, independent state slices, you want to use multiple useState() calls:

const Shop = props => {
const [cart, setCart] = useState([])
const [products, setProducts] = useState([]) // highlight-line
const [userData, setUserData] = useState({name: 'Max', age: 28}) // highlight-line
return ...
}

Just like that!

Each state will be independent of the other states and updating one state will therefore have no impact on the others.

Please also note that your state doesn't have to be an object!

Indeed, in our first example, it was an array (useState([])).

State can be anything when using React Hooks: An array, an object, a number, a string or a boolean.

If it is an object, just keep in mind that React won't merge your old state with your new state when you're setting it. You will always overwrite the old state and hence any merging that might be required has to be done by you!

#

Lifecycle Methods

So that is state management with React Hooks.

Another important reason for using classes was that you could add lifecycle methods to them - for example to load a list of products when your component gets rendered.

In functional components, you got no lifecycle methods - but of course you could make an Http request like this, can't you?

const Shop = props => {
const [products, setProducts] = useState([])
// highlight-start
fetch('my-backend.com/products')
.then(res => res.json())
.then(fetchedProducts => setProducts(fetchedProducts))
// highlight-end
return (
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}

Whilst this will kind of work, it's a horrible idea to update your state based on a side effect like this (here, the Http request is the side-effect)!

Why is this bad?

Whenever React re-renders your <Shop /> component, it will send this Http request. This already sounds pretty bad since chances are high that this component gets re-rendered for reasons that definitely don't require a new set of products. You might lose your old state if a user selected a product or a product got deleted locally in the meantime.

But in addition, you introduced an infinite loop because calling setProducts(...) will also cause the component to re-render - which in turn leads to another Http request and another setProduct(...) call. And so on...

In class-based components, the solution would've been simple:

componentDidMount() {
fetch('my-backend.com/products')
.then(res => res.json())
.then(fetchedProducts => this.setState({products: fetchedProducts}))
}

componentDidMount only executes once (=> when the component is mounted to the DOM for the first time) and hence you neither send too many Http requests nor do you create an infinite loop.

How can we solve this in functional components with React Hooks?

#

The useEffect() Hook

Besides useState(), there's another "core React Hook": useEffect().

This Hook should be used for any side-effects you're executing in your render cycle.

Here's how you use it:

const Shop = props => {
const [products, setProducts] = useState([])
// highlight-start
useEffect(() => {
fetch('my-backend.com/products')
.then(res => res.json())
.then(fetchedProducts => setProducts(fetchedProducts))
})
// highlight-end
return (
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}

useEffect() takes a function as an input and returns nothing. The function it takes will be executed for you after every render cycle.

And that is really important: After the render cycle. And after every render cycle. Both is important!

Therefore, the above solution is slightly better but the two main issues still persist:

  • The function inside of useEffect() gets executed unnecessarily (i.e. whenever the component re-renders)

  • We have an infinite loop because setProducts() causes the function to re-render

Thankfully, useEffect() has a solution! It takes a second argument which allows us to control when the function that was passed in as the first argument will be executed.

useEffect(() => {
fetch('my-backend.com/products')
.then(res => res.json())
.then(fetchedProducts => setProducts(fetchedProducts))
}, []) // highlight-line

Here, we pass an empty array ([]) as the second argument.

This leads React to only execute the function passed as the first argument when the component is rendered for the first time. Effectively, it now behaves like componentDidMount.

Both problems from above are now solved, we only fetch data when the component renders for the first time and we introduce no infinite loop!

#

What about componentDidUpdate?

Sometimes, you want to re-execute a certain side-effect (doesn't have to be a Http request of course) when the component updates.

Let's say, we want to fetch a specific product, identified by an id:

useEffect(() => {
fetch('my-backend.com/products/' + selectedId) // highlight-line
.then(res => res.json())
.then(fetchedProducts => setProducts(fetchedProducts))
}, [])

In this scenario, it's not desirable that the function inside of useEffect() only runs once. It should run whenever selectedId changes.

As a first step, we can remove the second argument we passed to useEffect(). This will cause the function to run whenever the component re-renders.

That certainly solves the problem of only one Http request being sent but it re-introduces another one: We now have an infinite loop again (because setProducts leads to a re-render cycle).

Of course, we only want to send a new Http request when selectedId changed, not when products changed (as set via setProducts).

That's where, again, the second argument for useEffect() kicks in:

useEffect(() => {
fetch('my-backend.com/products/' + selectedId)
.then(res => res.json())
.then(fetchedProducts => setProducts(fetchedProducts))
}, [selectedId]) // highlight-line

This time, we don't pass an empty array ([]) but an array that contains selectedId.

Because technically, we used kind of a special case/ behavior when we passed an empty array. This second argument (which always has to be an array) actually is simply a list of dependencies of your useEffect() function.

You can add any input to that array which should trigger the useEffect() function to run again.

You're essentially telling React:

"Hey React, here's an array of all dependencies of this function: The selectedId. When that changes, you should run the function again. Ignore any other changes to any other variable or constant."

In this case, that is selectedId - but you could "listen" to anything here!

Passing an empty array, as we did earlier, is kind of a "special case". When you pass an empty array, you're basically saying:

"Hey React, here's an array of all dependencies of this function - I got none. So please re-execute whenever any of these dependencies change. Since I have none, they can never change, so please never execute the function again".

So you're kind of tricking React here (not really - this is the intended way of running an effect only once).

#

Quick Summary

Thus far, we can replicate componentDidMount and componentDidUpdate - the two most important lifecycle methods we used in class-based components.

#

componentDidMount

useEffect(() => { ... }, [])
#

componentDidUpdate

Limit execution to the change of certain dependencies (comparable to manual diffing/ if checks in componentDidUpdate):

useEffect(() => { ... }, [dependency1, dependency2])

Alternatively, you run your effect on every re-render cycle:

useEffect(() => { ... })
#

What about componentWillUnmount and Cleanup Work?

Obviously, your components sometimes don't just need to do something when they (re-)render but also when they are about to be removed from the DOM. Cleaning up event listeners, Websocket connections or timers comes to mind.

In class-based components, you would typically use componentWillUnmount for that.

Here's the functional components + React Hooks equivalent:

useEffect(() => {
const timer = setTimeout(() => {
// do something amazing here
}, 2000)
// highlight-start
return () => {
// Cleanup work goes in here
clearTimeout(timer)
}
// highlight-end
})

The function passed as a first argument to useEffect() can return another function (or nothing, as we did in the previous examples).

If you return a function, that function will be executed right before the function passed to useEffect() runs. It also will be executed before the component is removed from the DOM.

Therefore, this returned function is the perfect place for cleanup work. Either during every re-render cycle (as written: Right before the "main" useEffect() function runs) or before the component disappears.

#

What about constructor, componentWillMount and componentWillUpdate?

With componentDidMount (link), componentDidUpdate (link) and componentWillUnmount (link), we covered the most important lifecycle methods and how you convert them to a functional component using React Hooks.

What about the constructor(), componentWillMount() and componentWillUpdate()?

Implementing constructor code into a functional component is easy.

If it should only execute once (when the component is created), use useEffect() like this:

const Shop = props => {
useEffect(() => {
// Initialization work
}, [])
return ...
}

So you're basically using componentDidMount here - which often is the better place for initialization work anyways.

If it doesn't matter how often the code runs (and it doesn't cause any side-effects), you can of course also just run it right in your normal functional component function body.

componentWillMount() and componentWillUpdate() shouldn't really be required since they had no real use-case anyways and were deprecated.

useEffect() (in the different configuration shown above) should be all you need.

#

Opting Out of Render Cycles with shouldComponentUpdate

One important lifecycle method that you might be missing thus far is shouldComponentUpdate(). We use this method in class-based components to define conditions under which we don't want to continue with the update cycle (potentially improving performance).

I won't discuss when checking for an opt-out might be beneficial (in short: if there is a high chance of regular, unnecessary re-render cycles) but I want to explain how you can opt out in functional components, too.

Up until React 16.6, this was simply not possible and you had to use class-based components if you wanted to add this optimization technique.

React 16.6 added a new method: React.memo().

This is not a Hook but simply a method that allows you to potentially remove your functional components from the rendering queue (for a given update).

Here's how you use it:

import React from 'react'
const Person = props => {
return <p>My name is {props.name}</p>
}
export default React.memo(Person) // highlight-line

<Person /> only depends on the name prop and hence it should only be re-evaluated by React if that prop changes. It should not be evaluated if its parent component changes but name stays the way it is (by default, it would be re-rendered, even if name didn't change).

By wrapping the whole component with React.memo(), you ensure that React only re-renders the component if the props used by the component changed => In this case, that would only be the name prop.

If you need more control (e.g. you're using three props in a component but you only want to re-render if two of them changed), you can also pass a second argument to React.memo(): An evaluation function that receives the old and new props as arguments and should return true if you want to re-render the component.

Learn more about React.memo() in the official docs.

#

Building your own Hooks

Besides being able to manage state and side effects in functional components, React Hooks also give you one additional powerful tool: You can write your own hooks and share them across components. The best thing is, that these custom Hooks can be stateful - i.e. you can use other Hooks (like useState()) in them.

Here's an example for a custom Hook that sends an Http request and returns the result as well as the loading state (is the request finished or not?). This example can also be found in the video at the beginning of this article.

import { useState, useEffect } from 'react'
export const useHttp = (url, dependencies) => {
const [isLoading, setIsLoading] = useState(false)
const [fetchedData, setFetchedData] = useState(null)
// fetch('https://swapi.co/api/people')
useEffect(() => {
setIsLoading(true)
console.log('Sending Http request to URL: ' + url)
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch.')
}
return response.json()
})
.then(data => {
setIsLoading(false)
setFetchedData(data)
})
.catch(err => {
console.log(err)
setIsLoading(false)
})
}, dependencies)
return [isLoading, fetchedData]
}

This custom Hook uses the Fetch API to send a request to a url that's passed into the Hook as an argument. It also receives the dependencies for useEffect() which is being used in the custom Hook.

And this custom Hook also returns something: isLoading and fetchedData.

This allows us to use the Hook like this in functional components:

const MyComponent = props => {
const [isLoading, fetchedData] = useHttp('https://swapi.co/api/people', []); // highlight-line
return isLoading ? <p>Loading...</p> : <DataOutput data={fetchedData} />;
}

It is worth noting, that custom Hooks - just like the built-in Hooks - should start with use. This signals, that it's a Hook and can be used as such. It also means that the Rules of Hooks (see below) apply.

What's the advantage of building your own Hooks?

You can share and re-use code much easier than you could before! The code shared in this example includes a lifecycle method (or: something that would've been handled in a lifecycle method in the past) and some internally managed state. And yet, it can be used in any functional component you want.

Please also note that your custom Hooks can take arguments but they don't have to. They also don't have to return something but they can. Just as the built-in Hooks. You got full flexibility.

#

The "Rules of Hooks"

So we got useState(), useEffect(), custom Hooks and all the other built-in Hooks.

It is important to know, that there are some rules that absolutely should be adhered to when working with Hooks - the so called "Rules of Hooks".

The important rule is: Only call Hooks at the top-level of your functional component!.

DON'T call them inside of if checks, for loops or any functions in your functional components. Only call them directly in your main functional component body or in your custom Hooks!

#

What Next?

React 16.8 added more than just these two Hooks. But useState() and useEffect() are by far the two most important ones and the two Hooks that you really need to write functional components everywhere (instead of class-based ones).

Recommended Courses