All You Need to Know About React Hooks

All You Need to Know About React Hooks

Master the use of React hooks
Ferenc AlmasiLast updated 2021 June 28 • Read time 19 min read
React hooks lets you use state and other features that previously were only possible with class components. Learn how they can simplify your code.
  • twitter
  • facebook
React

React hooks introduced in 16.8 lets you use state and other features that previously were only possible with class components. It lets you write less and cleaner code, which results in a better bundle size, with that a better performance and overall a better developer experience as well.

In this tutorial, we are going to look through the most commonly used hooks in React. In order they are:

At the end of the tutorial, we will also be taking a look at how you can write custom hooks. Before jumping into looking at what each hook does, let’s see what are the rules of using them.


Rules of Hooks

When it comes to using hooks, there are two things you need to keep in mind. First and foremost, you can only use hooks in functional components.

Copied to clipboard! Playground
// ✅ Valid use of hook
const hook = () => {
    const [state, setState] = useState({});
};

// ✅ Valid use of hook
function hook() {
    const [state, setState] = useState({});
}

// 🔴 Invalid use of hook
class Hook extends React.Component {
    const [state, setState] = useState({});
}
hooks.js

Secondly, only call hooks at the top level of your function. You can’t call them conditionally, use them in loops or in nested functions.

Copied to clipboard! Playground
// ✅ Valid use of hook
const hook = () => {
    const [state, setState] = useState({});
};

// 🔴 Invalid use of hook
const hook = () => {
    if (user.loggedIn) {
        const [state, setState] = useState({});
    }
};

// 🔴 Invalid use of hook
const hook = () => {
    searchResults.map(result => {
        const [state, setState] = useState(result);
    });
};
hooks.js

To automatically enforce these rules in your project, you can use the eslint-plugin-react-hooks linter. These are the only two things to keep in mind. Without any further ado, let’s start by taking a look at the useRef hook.


The useRef Hook

So how do you actually use hooks in React? To start things off, let’s take a look at one of the simplest hooks in react, called useRef. useRef is mostly used for storing references to DOM elements. You may already be familiar with React.createRef from class-based components. This hook behaves in a similar way. Let’s take a look at an example:

Copied to clipboard! Playground
import React, { useRef } from 'react';

const FocusInput = () => {
    const inputElement = useRef(null);

    const onClick = () => {
        inputElement.current.focus();
    };

    return (
        <React.Fragment>
            <input ref={inputElement} type="text" />
            <button onClick={onClick}>Focus input</button>
        </React.Fragment>
    );
};
useRef.js

As you can see, each hook in React is a function that uses the “use” word as a prefix. You can import them from React and what you pass to useRef will be your default value. You can then pass the reference to useRef into a ref prop to assign it to an element. To see the hook in action, you can try out the example on CodeSandbox.

Try the hook on codesandbox
Looking to improve your skills? Check out our interactive course to master React from start to finish.
Master Reactinfo Remove ads

The useState Hook

Let’s take a look at a more commonly used hook, useState. It is the equivalent of setState in class components.

Copied to clipboard! Playground
import React, { useState } from 'react';

const Input = () => {
    const [input, setInput] = useState('');
    const changeInput = e => setInput(e.target.value);

    return <input type="text" value={input} onChange={changeInput} />;
};
useState.js

Just as for useRef, whatever you pass to the function will be the initial value of your state. The function always returns an array with two elements:

  • The first element will always be your state.
  • The second element in the array will be a function that you can use to update your state.

In the example above, calling setInput with the value of e.target.value will update the input variable.

Try the hook on codesandbox

You can also use a function for the initial state:

Copied to clipboard! Playground
const Input = () => {
    const [input, setInput] = useState(() => getComputationHeavyState());
    const changeInput = e => setInput(e.target.value);

    return <input type="text" value={input} onChange={changeInput} />;
};
useState.js

This is useful for computation heavy calculations. This is because it is only executed once instead of on every single render. You can pass an updater function to the setInput function too, like so:

Copied to clipboard! Playground
const Input = () => {
    const [input, setInput] = useState('');
    const changeInput = e => setInput(e => e.target.value);

    return <input type="text" value={input} onChange={changeInput} />;
};
useState.js

In this case, it should return the updated value of the state.

Another important thing to know about is that unlike setState in class-based components, useState does not merge objects automatically. This means that if you have an object as a state, and you want to update a single property, you need to merge the rest of the objects manually.

Copied to clipboard! Playground
// ✅ Valid use of objects with useState
const [state, setState] = useState({
    name: 'John Doe',
    email: '[email protected]'
});

setState({
    ...state,
    name: 'Jane Doe'
});

// 🔴 Wrong use of objects with useState
const [state, setState] = useState({
    name: 'John Doe',
    email: '[email protected]'
});

setState({ name: 'Jane Doe' });
useState.js

Note that in the second example, the setState will override the whole object, and only leave the updated property in it. To fix it, use the first approach.

Another approach is to simply use two useState hooks, and separate the two properties into two different states. You can use as many useState hooks as you’d like, there’s no limit on them.

Copied to clipboard! Playground
const [name, setName]   = useState('John Doe');
const [email, setEmail] = useState('[email protected]');

setName('Jane Doe');
setEmail('[email protected]');
useState.js

So which approach should you take? It really depends and can be a personal preference. My approach is that if you have a complex state and you update multiple properties, go with an object. Otherwise split them up.


The useEffect Hook

useState is a great substitution of the setState function in class components. But what if you want to use lifecycle methods in functional components? You don’t have componentDidMount or componentWillUnmount in function-based components. With useEffect however, you can implement a similar behavior:

Copied to clipboard! Playground
import React, { useState, useEffect } from 'react';

export default function App() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        console.log('rendered');
    });

    return (
        <div className="App">
            <div>{Array(count).fill('🥑').join(',')}</div>
            <button onClick={() => setCount(count + 1)}>Add avocado</button>
        </div>
    );
}
useEffect.js

As you can see, useEffect expects a callback function as a parameter. This example displays a list of avocados. Every time I click the button, the component gets re-rendered, and the function is called. So how can you make it so that it is only called once, like componentDidMount? You need to pass an empty array as the second parameter.

Copied to clipboard!
useEffect(() => {
    console.log('component mounted');
}, []);
useEffect.js

The second parameter of useEffect is a dependency array. This means that the effect does not depend on any props or state, so it should never re-run.

If you do need to update the component and run the effect on state or props change, you can pass them to your dependency array, and it will only get re-rendered if any of those values change.

Copied to clipboard!
useEffect(() => {
    console.log('rendered');
}, [count]);
useEffect.js

Be careful with changing dependencies inside the effect! This can cause infinite loops.

Also note, that when changes are made to the values inside the dependencies array, React does a shallow comparison, so keep that in mind when dealing with objects.

You can also extend the callback function inside the useEffect with a return statement to clean things up. You can think of things such as removing event listeners, unsubscribing from events, and making sure that unused memory is freed up.

Copied to clipboard! Playground
useEffect(() => {
    console.log('component mounted');

    return () => {
        console.log('component unmounted');
    };
}, []);
useEffect.js
This will be called when a component is destroyed, that is, it acts as an unmount.

A common way for using the useEffect hook is to use it for fetching data. If you would like to learn more about how to fetch data using the useEffect hook, I suggest checking out the article below which goes into more detail.

How To Easily Fetch Data With React HooksTry the hook on codesandbox

The useCallback Hook

Next up, we have the useCallback hook. This hook is useful when you want to memoize a callback function, that only changes if one of its dependencies change. This can be used to optimize child components where you want to reduce the amount of re-renders because of a callback function. Take the following as an example:

Copied to clipboard! Playground
import React, { useState } from 'react';

const Avocado = () => {
    const [count, setCount] = useState(0);

    const addAvocado = () => {
        setCount(count + 1);
    };

    return (
        <React.Fragment>
            <Addvocado add={addAvocado} />
            <div>{Array(count).fill('🥑').join(',')}</div>
        </React.Fragment>
    );
};

const Addvocado = ({ add }) => {
    console.log('component re-rendered');

    return <button onClick={add}>Add avocado</button>;
};
useCallback.js

In the example above, whenever you click the button, it increases the number of avocados by one. This re-renders the child component each time, even though there is no state change inside of it.

Child component re-renders, everytime the button is clicked.

To fix this, we can use useCallback. Rewrite the addAvocado function in the following way:

Copied to clipboard! Playground
import React, { useState, useCallback } from 'react';

...

const memoizedAddvocado = useCallback(() => {
    setCount((c) => c + 1);
}, [setCount]);
useCallback.js

This will only change the function, whenever setCount is changed. Now since the function is memoized this way, we can’t update the count using setCount(count + 1). Instead, we can make use of the updater function.

Now this alone won’t fix the issue, you also need to wrap the child component to React.memo to tell React, it should only be re-rendered if one of the passed props change. Change the Addvocado function to the following:

Copied to clipboard! Playground
const Addvocado = React.memo(({ add }) => {
    console.log('component re-rendered');

    return <button onClick={add}>Add avocado</button>;
});
useCallback.js

Now the component renders only once. Keep in mind, however, that you should only optimize renders if the performance of your app degrades.

Child component only renders once. Try the hook on codesandbox

The useMemo Hook

Another React hook that can be used for optimization purposes is the useMemo hook. While useCallback memoize callbacks, useMemo can be used to memoize values.

Copied to clipboard!
import { useMemo } from 'react';

const memoizedValue = useMemo(() => getFibonacciArray(length), [length]);
useMemo.js

This hook expects a function that returns the computed value. The second parameter is again the dependency list, just as for other hooks.

Note that useMemo(() => fn, []) is equivalent to useCallback(fn, []).

Just as for useCallback, only use useMemo if you experience performance issues. Don’t optimize prematurely.

Looking to improve your skills? Check out our interactive course to master React from start to finish.
Master Reactinfo Remove ads

The useReducer Hook

Let’s take a look at two other more complex hooks. One of them is useReducer that is an alternative of useState. It is most useful when you are dealing with complex state. It works similar to Redux. It accepts two parameters, a reducer function and an initial state:

Copied to clipboard! Playground
import { useReducer } from 'react';

const reducer = (state, action) => {

};

const App = () => {
    const [state, dispatch] = useReducer(reducer, initialState);

    return (...);
};
useReducer.js

And it returns two values. Your state, and a dispatch function that you can use to call the reducer function.

Note that you should define the reducer function outside of your component. This is a function that will hold all your logic of updating the state. This should be a pure function, so avoid mutating state. It accepts two parameters:

  • state: This will be your current state.
  • action: This will get the value of whatever you pass into the dispatch function that is returned from useReducer.

It should always return your new state. So what goes inside the reducer function? Basically any logic that updates your state, but most commonly you’ll see a switch statement used in the following way:

Copied to clipboard! Playground
const reducer = (state, action) => {
    switch (action.type) {
        case 'add':
            return state + 1;
        case 'remove':
            return state - 1;
        default:
            return state;
    }
};

const App = () => {
    const [state, dispatch] = useReducer(reducer, 0);

    return (
        <React.Fragment>
            <button onClick={() => dispatch({ type: 'add' })}>Addvocado</button>
            <button onClick={() => dispatch({ type: 'remove' })}>Removocado</button>
            <div>{Array(state).fill('🥑').join(',')}</div>
        </React.Fragment>
    );
};
useReducer.js

Anything you pass to dispatch, will be your action. Note that although we pass an object here, you can also pass primitives. The following is equivalent to the above:

Copied to clipboard! Playground
const reducer = (state, action) => {
    switch (action) {
        case 'add':
            return state + 1;
        case 'remove':
            return state - 1;
        default:
            return state;
    }
};

const App = () => {
    const [state, dispatch] = useReducer(reducer, 0);

    return (
        <React.Fragment>
            <button onClick={() => dispatch('add')}>Addvocado</button>
            <button onClick={() => dispatch('remove')}>Removocado</button>
            <div>{Array(state).fill('🥑').join(',')}</div>
        </React.Fragment>
    );
};
useReducer.js

So when should you use useReducer and when should you use useState? In case you know you will be dealing with complex state, then go with useReducer. Otherwise, you can get away with useState.

Try the hook on codesandbox

The useContext Hook

Lastly, I wanted to cover the useContext hook, which makes use of the context API in React. If you used the context API before, useContext is equivalent to static context = UserContext in class-based component.

It lets you read a context that is shared between components, and subscribe to its changes. To see it in action, take the following example:

Copied to clipboard! Playground
import React from 'react';
import { BrowserRouter, Route, Link } from 'react-router-dom';

import { UserProvider } from './Context';
import Dashboard from './Dashboard';
import Settings from './Settings';

export default function App() {
    const user = {
        name: 'John Doe',
        email: '[email protected]'
    };

    return (
        <BrowserRouter>
            <ul>
                <li><Link to="/">Dashboard</Link></li>
                <li><Link to="/settings">Settings</Link></li>
            </ul>
            <UserProvider value={user}>
                <Route path="/" exact component={Dashboard} />
                <Route path="/settings" component={Settings} />
            </UserProvider>
        </BrowserRouter>
    );
}
useContext.js

Here we have two routes, a dashboard and a settings page. Both of the routes display only the title of the page:

Copied to clipboard!
import React from 'react';

export default () => <h1>Dashboard</h1>;
Dashboard.js

I’ve wrapped these routes into a UserProvider component, which I’ve imported from UserContext.js. This file exports a new context and its provider:

Copied to clipboard! Playground
import { createContext } from 'react';

const UserContext = createContext({});
const UserProvider = UserContext.Provider;

export { UserContext, UserProvider };
UserContext.js
I've set the context to an empty object by default.

To use this context with the useContext hook inside the routes, all you need to do, is import the context, and pass it into useContext hook:

Copied to clipboard! Playground
import React, { useContext } from 'react';

import { UserContext } from './Context';

export default () => {
    const user = useContext(UserContext);

    return (
        <React.Fragment>
            <h1>Dashboard</h1>
            <span>Logged in as {user.name} ({user.email})</span>
        </React.Fragment>
    );
};
Dashboard.js

You can use this state wherever you need it using useContext. To also allow changes to the state and update components where it is used, you can pass the user object to useState and pass its values down to the provider.

Copied to clipboard! Playground
import React, { useState } from 'react';

export default function App() {
    const [state, setState] = useState(user);

    return (
        ...
        <UserProvider value={[state, setState]}>
            <Route path="/" exact component={Dashboard} />
            <Route path="/settings" component={Settings} />
        </UserProvider>
    );
}
useContext.js

Then use it, where appropriate. If you now call setUser in one of your routes, the other routes will be updated too.

Copied to clipboard! Playground
import React, { useContext } from 'react';

import { UserContext } from './Context';

export default () => {
    const [user, setUser] = useContext(UserContext);

    const changeEmail = () => {
        setUser({
            ...user,
            email: '[email protected]'
        });
    };

    return (
        <React.Fragment>
            <h1>Settings</h1>
            <span>Logged in as {user.name} ({user.email})</span>
            <br />
            <button onClick={changeEmail}>Change email</button>
        </React.Fragment>
    );
};
Settings.js

If you need to optimize the number of renders, you can also throw a useMemo hook into the mix to add memoization.

Try the hook on codesandbox

Creating Custom Hooks

As mentioned at the beginning of this tutorial, I want to end things with a custom hook. React makes it possible to implement your own hooks to get the most out of them. To see how this can be done, let’s take a look at a common problem: displaying a loading state while data is fetched from an API. To keep it true to the naming convention, we will call it useFetch. To see how we want the hook to behave, let’s use it first, then we will write the function for it:

Copied to clipboard! Playground
import React from 'react';

import useFetch from './useFetch';

const App = () => {
    const [post, loading] = useFetch("https://jsonplaceholder.typicode.com/posts/1");

    return (
        <div className="App">
            <div>{loading ? "loading..." : post}</div>
        </div>
    );
};
App.js

We want to make it accept a URL, and it should return two things: the body of the post and a state about whether the data is loading. We then display either a loading message or the post itself. Now let’s see how this can be implemented. Create a new file called useFetch.js and add the following function:

Copied to clipboard! Playground
import { useState, useEffect } from 'react';

export default url => {
    const [state, setState] = useState([null, false]);

    useEffect(() => {
        setState([null, true]);

        (async () => {
            const data = await fetch(url).then(res => res.json());

            setState([data.body, false]);
        })();
    }, [url]);

    return state;
};
useFetch.js

This custom hook uses useState and useEffect internally. When the useFetch is called, we set the loading flag to true, then after the response has arrived, we set it back to false, and set the null to the body of the response. Lastly, the state is returned from the function.

Note that you can’t directly make the useEffect hook’s callback function to async. Instead, I created an IIFE. Don’t forget to pass the url as a dependency to useEffect. And you’ve successfully created your very first custom React hook!

Try the hook on codesandbox

Summary

And that is everything you need to know about React hooks. If you’ve reached this far, you should now have a strong understanding of how hooks work, how and which you need to use for a given problem, or even how to create your own ones.

Do you have any additional thoughts about React hooks? Let us know in the comments below! Thank you for reading through, happy coding!

If you want to learn more about React, check out how you can use Suspense to further improve the way you fetch data:

How to Improve Data Fetching in React With Suspense?
  • twitter
  • facebook
React
Did you find this page helpful?
📚 More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access 100+ interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Prepare for technical interviews
Become a Pro

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.