React Hooks

React Hooks

React has come a long way since its beginning back in May 2013 when it was made open source. While the core concepts of the framework have remained the same, the implementation is always improving to help developers like yourself easily set up a SPA application. Features like State and Lifecycle Methods allow you to maintain data and route users around your website with ease with two types of components. These components are known as Class components (stateful), and functional components (stateless/display).

Sidenote: You should have an understanding of React classes, state, life-cycle methods, and Context before learning hooks. While it's true that hooks will enhance and even replace some of these features, they still work the same and a base understanding of these concepts are essential. It is also helpful to be comfortable with es6 destructuring and literals as I will use them often on Objects and Arrays.

Hooks

Hooks were recently introduced to React to allow you to use things like state and lifecycle methods within a functional component. The hooks included are the following:

  • useState
  • useEffect
  • useContext
  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

Wow that's quite the list right!? No worries, we will mostly be using the first 3 in the apps we make, so this article will focus on those three. However the others are just as useful and definitely deserve attention when you feel more comfortable with the concept. The Reactjs.org docs have documentation on all of them, which can be found here.
React Hooks API reference

useState

Use state is one of the most used hooks as it allows you to do exactly what it is called! You can now useState inside of a functional component which means class components are no longer necessary for you to have state in your application! What?! Yeah that's right, no more classes, no more class this context, no more constructor, super, render, or extends Component.

Sidenote: There is not yet a React hook that let's us do the functionality of componentDidCatch, so for that error handler we will still use a class. The React team are planning on adding hooks eventually to manage this, so stay tuned on the React Docs.

We can do everything we've been doing with state within a function component. So let's see this in action. First, here is a class component that maintains a count in state and has a method that allows you to increment that value in state.

import React, { Component } from 'react'

class App extends Component {
    constructor(){
        super()
        this.state = {
            count: 0
        }
    }

    increment = () => {
        this.setState(prevState => ({
            count: prevState.count + 1
        }))
    }

    render(){
        return (
            <div>
                <h1>{this.state.count}</h1>
                <button onClick={this.increment}>Increment</button>
            </div>
        )
    }
}

export default App

Now here is that same functionality using the useState hook in a function component:

import React, { useState } from 'react'

const App = () => {
    const [count, setCount] = useState(0)
    return (
        <div>
            <h1>{count}</h1>
            <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
    )
}

export default App

That is quite a difference! Let's break this down and talk about the parts.

First we imported { useState } from 'react'. All hooks are imported this way as a destructured part of the React package.

Next we have this line:

  • const [count, setCount] = useState(0)

Let's break it down into it's parts:

  • useState(0)
    • useState is a method that returns and array with two items inside.
    • Whatever we put in the parenthesis is the initial state value, so here I am initializing the state at the number 0.
  • const [count, setCount] =
    • Using ES6 array destructing, we declare state and setState variables. The first variable is the repsentation of our state, so count will be a variable currently pointing to the number 0.
    • setCount is the setter method given by useState, and can be seen as the setState method. We will use this method to update our count state variable. It is convention to prefix the method name with the word set, followed by the part of state it will be setting.

Unlike the state in a class component, useState will take ANY datatype as it's initial state value! This means we no longer have to use an object to represent state if we don't need.
* Just like setState, all of the setter methods provided by useState allows for a prevState callback that provides the prevState value of the state variable.

  • Lastly we are using the count variable to display the count in the <h1>, and are using the setCount method to update out count in the button click.

Hook Rules

Before we get any further, we should quickly cover the rules around hooks. As cool as they are, they do have some restrictions that we need to be aware of.

Hooks can ONLY be used at the top level of our Component declaration, so you should not be declaring any variables with hooks inside of your return statement. This means you should NOT declare them within:

  1. loops(map)
  2. nested functions
  3. or as a part of a conditional statement.

If you break any of these rules, you will run into errors that crash your application.

Also we need to be aware that the set method takes information to REPLACE the current state variable, so if our initial state is something like an object or an array, we have to spread in previous state to maintain data as pieces of the data changes. For example, here is an array of string names that is our initial state, and a button that is adding a name to it.

import React, { useState } from 'react'

const ArrExample = () => {
    const [names, setNames] = useState(["joe", "steve", "carol"])

    return (
        <div>
            <div>
                { names.map((name, i) => <h1 key={i}>{name}</h1>) }
            </div>
            <button onClick={() => setNames(prev => [...prev, "frank"])}>Add Frank</button>
        </div>
    )
}

export default ArrExample

If you copy and look at this code in your web browser, you will see "joe", "steve", and "carol" listed on the DOM. When you click the button, setNames spreads the existing names into the new array and add's "frank" to the end of the list. This is the exact same thing you would do with objects, so to see an example of this, let's look and our common handleChange function we would write to handle <form> input changes.

import React, { useState } from 'react'

const Form = () => {
    const [inputs, setInputs] = useState({title: "", description: ""})
    
    const handleChange = e => {
        const {name, value} = e.target
        setInputs(prevInputs => ({
            ...prevInputs, 
            [name]: value
        }))
    }
  
    return (
        <form>
            <input 
                type="text" 
                name="title" 
                value={inputs.title} 
                onChange={handleChange}/>
            <input 
                type="text" 
                name="description" 
                value={inputs.description} 
                onChange={handleChange}/>
            <button>Submit</button>
        </form>
    )
}

export default Form

The point of this example is to see what our setInputs method needs to appropriately update the inputs. First we need to spread ... in the existing inputs from prev (previous State), and then overwrite the input the user is typing in by using the [e.target.name]: e.target.value syntax.

Also notice that I wrote handleChange as a nested function of our Form component, this will be a normal pattern when you are using hooks as we no longer have a class to write methods in.

That is really it with useState, functionally it's very simliar to state and setState in a class component, but allows us to break state up into different parts or manage them together.

More on the useState hook can be found here.


useEffect

Now that we have covered state, what other peice from class components do we need to account for if we are no longer using classes? The answer, lifecycle methods. How do we do our axios.get request or window.addEventListener in componentDidMount if there is no class?

useEffect is both componentDidMount, componentDidUpdate, and componentWillUnmount all bundled into one. useEffect is for performing side effects on your component. Side effect include HTTP requests(data fetching), manual DOM manipulation(transitions on enter/exit/change), event listeners, and subscriptions.

HTTP requests

Depending on the side effect you are using, you may have to clean up the effect when the component unmounts. Let's first look at a side effect that doesn't need clean up, such as a componentDidMount HTTP request using axios. First, here is how we normally would have fetched data and saved it in state before hooks:

import React, { Component } from 'react'
import axios from 'axios'

class App extends Component {
    constructor(){
        super()
        this.state = {
            data: []
        }
    }

    componentDidMount(){
        axios.get('https://api.vschool.io/joeschmoe/todo')
            .then(res => this.setState({ data: res.data }))
            .catch(err => console.log(err))
    }

    render(){
        return (
            <div>
                { this.state.data.map(item => <h1 key={item._id}>{item.title}</h1>) }
            </div>
        )
    }
}

export default App

Now, here is the same request being done with useEffect and saving the data with useState:

import React, { useEffect, useState } from 'react'
import axios from 'axios'

const App = () => {
    const [data, setData] = useState([])

    useEffect(() => {
        axios.get('https://api.vschool.io/natej/todo')
            .then(res => setData(res.data))
            .catch(err => console.log(err))
    }, [])

    return (
        <div>
            { data.map(item => <h1 key={item._id}>{item.title}</h1>) }
        </div>
    )
}

export default App

Let's break down this useEffect into parts:

  • useEffect(() => {
    • useEffect takes an anonymous function to fire componentDidMount and/or componentDidUpdate.
  • }, []):
    • This second argument [] is for telling the useEffect hook whether or not to fire again. A value could be placed here in the array that useEffect will check and see if it has changed. If so, it will fire the hook again to re-update state (replacing componentDidUpdate). Since we do not want this to run more than on componentDidMount, we provide an empty array [] so that there is never a change in values. If you remove this [] from the above example, componentDidMount will fire repeatedly causing our axios request to repeat itself indefinitely!

useEffect by default will fire after every render and update that happens in our app, so the only way to tell it not to re-run is by using the [] empty array or by placing a value for it to check to know whether to run again.


Events (and other side effects needing cleanup)

Great, so now let's look at useEffect in the context of componentDidMount & componentWillUnmount when we need to use both to do some clean up on our component. In the following example, we will add a window event listener on componentDidMount, and remove it on componentWillUnmount using the useEffect hook.

import React, { useEffect, useState } from 'react'

const App = () => {
    const [color, setColor] = useState("blue")

    useEffect(() => {
            // componentDidMount
        window.addEventListener('keydown', handleKeyDown)
        return () => {
            // componentWillUnmount
            window.removeEventListener('keydown', handleKeyDown)
        }
    })

    const handleKeyDown = e => {
        if(e.which === 71){
            setColor('green')
        } else if(e.which === 82){
            setColor('red')
        } else {
            setColor('blue')
        }
    }

    return <div style={{backgroundColor: color, width: 200, height: 200}}></div>
}

export default App

This example sets up a window event listener for keydown events, and changes the state of our color that affects the color of the displaying div. To use componentWillUnmount, you must return an anonymous function in your useEffect hook as you can see in the above example: return () => {}

Other common side effects that you would need componentWillUnmount for are things like setInterval, setTimeout, and subscribing/unsubscribing from services.

More on the useEffect hook can be read here.


useContext

Working in React comes with certain methods we have to repeat, such as writing HOC methods to make the <Consumer> of a context reuseable for all components that need it. useContext instead provides a way to consume your context values without having to use HOC's or extra component wrappers! (insert nerd celebration here). Let's see how it works. This first file is my Context file that provides both a CountProvider wrapper for the value, and a useCount custom hook I can use to consume that Provider value.

The pattern used below for useContext and other hooks was partly taken from Kent C. Dodds explanation on this topic. I definitely suggest checking out his work on this here: State Management

// CountProvider.js
import React, { useState, useContext, useMemo } from 'react'

// We first create our Context Variable
const CountContext = React.createContext()

// Then we can declare our Provider wrapper component
const CountProvider = props => {
    const [count, setCount] = useState(0)
    const value = useMemo(() => [count, setCount], [count])
    return (
        <CountContext.Provider value={value} {...props}/>
    )
}

// Next we can declare our Consumer function by creating a custom hook!
const useCount = () => {
    const context = useContext(CountContext)
    if(!context){  // If unable to consume context, crash the program and let me know.
        throw new Error("You must use CountProvider to consume Count Context")
    }
    return context
}

// We can then export both the Provider wrapper, and the custom hook for when need to consume the provider value.
export { CountProvider, useCount }

There are a few things going on in this file, starting with the CountProvider component:

  • First we use useState to create a count state variable and setter method.
  • Next we use useMemo to memoize this function. useMemo is for performance optimization as it will remember the count value we put in the [count] array brackets, and if it has encountered that update in the past it will re-use that computed value rather than re-compute and run the functions again. useMemo is returning the same array [count, setCount] and storing it in the value variable.
  • Lastly we return the CountContext.Provider component with the value and props passed into it.

Next we have the useCount custom hook: (this is replacing our typical HOC function which would have called withCount:

  • First we declared the function with the convention of use to name our function useCount.
  • Next, we use useContext to grab the context variable from the top of the page. When useContext is given a context variable, it returns to you the value object from the Provider, so no HOC's or <Consumer> components have to be explicitely used. We're letting React do that for you.
  • Lastly, we first check to make sure we were able to get the context and throw and error if we didn't. If it works, we return the context value object.

Now let's talk about some big differences. We are exporting two functions from this file, CountProvider and useCount. <CountProvider/> can still be wrapped around the <App/> component in our index.js, or it can be wrapped around the section of JSX that needs access to it's value object. In this case, I will just wrap it around the App component.

// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.js'
import {CountProvider} from './context/CountProvider.js'

ReactDOM.render(
    <CountProvider>
        <App />
    </CountProvider>, 
document.getElementById('root'))

It is important to know that the Provider component does NOT have to be wrapped this high up in our Component tree if only a sub-section of our application needs the ability to consume. So you could import the CountProvider component a couple levels deep and wrap your JSX that needs to be able to consume.

Then in our App.js, we can import the useCount custom hook to get access to the CountProvider's value object.

// App.js 
import React from 'react'
import { useCount } from './context/CountProvider.js'

const App = () => {
    const {count, setCount} = useCount()
    return (
        <div>
            <h1>{count}</h1>
            <button onClick={() => setCount(count + 1)}> Increment </button>
            <button onClick={() => setCount(count - 1)}> Decrement </button>
        </div>
    )
}

export default App

As we can see, our self-made useCount() hook returns the variable and the setter method from our Context, the only difference is we do not have to access props to get this information like you do when you wrap the export in a HOC function.


Congrats! You have seen the 3 main hooks (plus useMemo) React just introduced and can begin to use them in your own code. They are safe to use as they are fully integrated into React as of the most recent update (late 2018 - early 2019). As you become more comfortable you should definitely check out the other hooks they are providing. Here is a summary of what they do:

  • useReducer:

    • Have you ever used redux? This is for you. You write a reducer function as you would with redux and provide it to useReducer(myReducer). In return, it gives you the store and the dispatch method!
  • useCallback

    • Uses memoization to cache and optimize a specific callback function.
  • useMemo

    • Also uses memoization to cache expensive create functions and only calls it again if the values used in the create function have changed.
  • useRef

    • Used to assign a React ref to a variable without the ref callback.
  • useImperativeHandle

    • A way to customize the instance value of a ref (not typically used, but availabled if needed when forwarding Refs).
  • useLayoutEffect

    • Similar to useEffect but it fires synchronously after all DOM mutations. This is why useEffect should be used for layout effects (animations, transitions) when possible.
  • useDebugValue

    • When you make your own custom hooks that use React hooks, this allows you to label your custom hook in dev-tools.

Lastly, the only hooks that don't exist yet are for componentDidCatch in Error Boundaries, and getDerivedStateFromError. This means you would still need to use the React.Component in a class to use those features until further updates bring us more hooks!