DEV Community

Cover image for 5 React Custom Hooks You Should Start Using (Explained)
Grégory D'Angelo for AlterClass

Posted on • Edited on • Originally published at alterclass.io

5 React Custom Hooks You Should Start Using (Explained)

Are you repeating yourself by building the same features again and again inside your functional components? Then, in this video, we gonna cover the 5 custom hooks that I use every single day in most of my React applications and that you should also use.

Those 5 React hooks will boost your productivity, speed up your development process, and save you a lot of time so you can work on more valuable features for your product or application.

So let's dive in!

Watch the video on Youtube or keep reading.


Table of content


React hooks

Introducing React Hooks

React hooks have been introduced to the library with version 16.8. It allows you to use state and other React features in your functional components so that you don't even need to write classes anymore.

In reality, hooks are much more than that.

Hooks let us organize the logic inside a component into reusable isolated units.

They are a natural fit for the React component model and the new way to build your applications. Hooks can cover all use cases for classes while providing more flexibility in extracting, testing, and reusing code throughout your application.

Building your own custom React hooks, you can easily share features across all components of your applications and even across different applications, so you don't repeat yourself and get more productive at building React applications.

Right now, we're going to take a look at my top 5 custom hooks, re-create them from scratch together, so you really understand how they work and exactly how you can use them to boost your productivity and speed up your development process.

So let's jump right into building our first custom React hook.

useFetch

How many times have you built a React application that needs to fetch data from an external source before rendering it to the users?

Fetching data is something I'm doing every single time when I'm building a React application. I even make several fetching calls inside a single application.

And whatever the way you choose to fetch your data, either with Axios, the Fetch API, or anything else, you are always writing the same piece of code again and again across your React components and across your applications as well.

So let's see how we can build a simple yet useful custom Hook we can call whenever we need to fetch data inside our application.

This way, we'll be able to reuse the logic inside that React hook in any functional components to fetch data with just one line of code.

Okay. So let's call our custom hook: useFetch.

This hook accepts two arguments, the URL we need to query to fetch the data and an object representing the options we want to apply to the request.

import { useState, useEffect } from 'react';

const useFetch = (url = '', options = null) => {};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Fetching data is a side effect. So we should use the React useEffect hook to perform our query.

In this example, we are going to use the Fetch API to make our request. So we are going to pass the URL and the options. And once the Promise is resolved, we retrieved the data by parsing the response body. For that, we use the json() method.

Then, we just need to store it in a React state variable.

import { useState, useEffect } from 'react';

const useFetch = (url = '', options = null) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url, options)
      .then(res => res.json())
      .then(data => setData(data));
  }, [url, options]);
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Okay, but we should also catch and handle network errors in case something goes wrong with our request. So we gonna use another state variable to store the error. So we could return it from our hook and be able to tell if an error has occurred.

import { useState, useEffect } from 'react';

const useFetch = (url = '', options = null) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url, options)
      .then(res => res.json())
      .then(data => {
        if (isMounted) {
          setData(data);
          setError(null);
        }
      })
      .catch(error => {
        if (isMounted) {
          setError(error);
          setData(null);
        }
      });
  }, [url, options]);
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Our useFetch hook would return an object containing the data fetched from the URL or the error if anything wrong happened.

return { error, data };
Enter fullscreen mode Exit fullscreen mode

Finally, it is generally a good practice to indicate to your users the status of an asynchronous request, such as displaying a loading indicator before rendering the results.

So let's add a third state variable to our custom hook to track our request's status. We set this loading variable to true right before launching our request, and we set it back to false once it is done.

const useFetch = (url = '', options = null) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);

    fetch(url, options)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setError(null);
      })
      .catch(error => {
        setError(error);
        setData(null);
      })
      .finally(() => setLoading(false));
  }, [url, options]);

  return { error, data };
};
Enter fullscreen mode Exit fullscreen mode

We can now return this variable with the others to use it in our components to render a loading spinner while the request is running so that our users know that we are getting the data they asked for.

return { loading error, data };
Enter fullscreen mode Exit fullscreen mode

One more thing before we see how to use our new custom hook.

We need to check if the component using our hook is still mounted to update our state variables. Otherwise, we are introducing memory leaks in our application.

To do that, we can simply create a variable to check if our component is still mounted and use the cleanup function to update this variable when the component unmounts. And inside the Promise methods, we can first check if the component is mounted before updating our state.

import { useState, useEffect } from 'react';

const useFetch = (url = '', options = null) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let isMounted = true;

    setLoading(true);

    fetch(url, options)
      .then(res => res.json())
      .then(data => {
        if (isMounted) {
          setData(data);
          setError(null);
        }
      })
      .catch(error => {
        if (isMounted) {
          setError(error);
          setData(null);
        }
      })
      .finally(() => isMounted && setLoading(false));

    return () => (isMounted = false);
  }, [url, options]);

  return { loading, error, data };
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Alright! Now, let's see how easy it is to fetch data with our useEffect hook.

We just need to pass the URL of the resource we want to retrieve. From there, we get an object that we can use to render our application.

import useFetch from './useFetch';

const App = () => {
  const { loading, error, data = [] } = useFetch(
    'https://hn.algolia.com/api/v1/search?query=react'
  );

  if (error) return <p>Error!</p>;
  if (loading) return <p>Loading...</p>;

  return (
    <div>
      <ul>
        {data?.hits?.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

useEventListener

Let's move into our second custom hook: useEventListener.

This hook is responsible for setting up and clean up an event listener inside our components.

This way, we don't need to repeat ourselves every time we need to add event listeners to our application.

It accepts as arguments the name of the event we want to listen for, the function to run whenever an event of the specified type occurs, the target under which to listen for the event, and finally, a set of options for the event listener.

import { useEffect, useRef } from 'react';

const useEventListener = (
  eventType = '',
  listener = () => null,
  target = null,
  options = null
) => {};

export default useEventListener;
Enter fullscreen mode Exit fullscreen mode

As with the previous hook, we will use the React useEffect hook to add an event listener. But first, we need to make sure that the target supports the addEventListener methods. Otherwise, we do nothing!

import { useEffect, useRef } from 'react';

const useEventListener = (
  eventType = '',
  listener = () => null,
  target = null,
  options = null
) => {

  useEffect(() => {
    if (!target?.addEventListener) return;
  }, [target]);
};

export default useEventListener;
Enter fullscreen mode Exit fullscreen mode

Then, we can add the actual event listener and remove it inside the cleanup function.

import { useEffect, useRef } from 'react';

const useEventListener = (
  eventType = '',
  listener = () => null,
  target = null,
  options = null
) => {
  useEffect(() => {
    if (!target?.addEventListener) return;

    target.addEventListener(eventType, listener, options);

    return () => {
      target.removeEventListener(eventType, listener, options);
    };
  }, [eventType, target, options, listener]);
};

export default useEventListener;
Enter fullscreen mode Exit fullscreen mode

Actually, we gonna also use a reference object to store and persist the listener function across renders. We will update this reference only if the listener function changes and use this reference inside our event listener methods.

import { useEffect, useRef } from 'react';

const useEventListener = (
  eventType = '',
  listener = () => null,
  target = null,
  options = null
) => {
  const savedListener = useRef();

  useEffect(() => {
    savedListener.current = listener;
  }, [listener]);

  useEffect(() => {
    if (!target?.addEventListener) return;

    const eventListener = event => savedListener.current(event);

    target.addEventListener(eventType, eventListener, options);

    return () => {
      target.removeEventListener(eventType, eventListener, options);
    };
  }, [eventType, target, options]);
};

export default useEventListener;
Enter fullscreen mode Exit fullscreen mode

We don't need to return anything from this hook as we are just listening for events and run the handler function pass in as an argument.

It is now easy to add an event listener to our components, such as the following component, to detect clicks outside a DOM element. Here we are closing the dialog component if the user clicks outside of it.

import { useRef } from 'react';
import ReactDOM from 'react-dom';
import { useEventListener } from './hooks';

const Dialog = ({ show = false, onClose = () => null }) => {
  const dialogRef = useRef();

  // Event listener to close dialog on click outside element
  useEventListener(
    'mousedown',
    event => {
      if (event.defaultPrevented) {
        return; // Do nothing if the event was already processed
      }
      if (dialogRef.current && !dialogRef.current.contains(event.target)) {
        console.log('Click outside detected -> closing dialog...');
        onClose();
      }
    },
    window
  );

  return show
    ? ReactDOM.createPortal(
        <div className="fixed inset-0 z-9999 flex items-center justify-center p-4 md:p-12 bg-blurred">
          <div
            className="relative bg-white rounded-md shadow-card max-h-full max-w-screen-sm w-full animate-zoom-in px-6 py-20"
            ref={dialogRef}
          >
            <p className="text-center font-semibold text-4xl">
              What's up{' '}
              <span className="text-white bg-red-500 py-1 px-3 rounded-md mr-1">
                YouTube
              </span>
              ?
            </p>
          </div>
        </div>,
        document.body
      )
    : null;
};

export default Dialog;
Enter fullscreen mode Exit fullscreen mode

useLocalStorage

For our third custom hook, we will leverage the localStorage of our browser to persist our component's state across sessions.

For this one, we need the name of the key to create or update in localStorage and an initialValue. That's it!

import { useState } from 'react';

const useLocalStorage = (key = '', initialValue = '') => {};

export default useLocalStorage;

Enter fullscreen mode Exit fullscreen mode

And we are going to return an array like the one you get with the React useState hook. So this array will contain a stateful value and a function to update it while persisting it in localStorage.

So let's dive in.

First, let's create the React state variable we will sync with localStorage.

import { useState } from 'react';

const useLocalStorage = (key = '', initialValue = '') => {
  const [state, setState] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(error);
      return initialValue;
    }
  });
};

export default useLocalStorage;
Enter fullscreen mode Exit fullscreen mode

Here we are using lazy initialization to read 'localStorage' to get the key's value, parse the value if any has been found, or return the initial value passed in as the second argument to our hook.

In case something goes wrong while reading in localStorage, we just log an error and return the initial value.

Finally, we need to create the update function to return that it's going to store any state's updates in localStorage rather than using the default one returned by the useState hook.

import { useState } from 'react';

const useLocalStorage = (key = '', initialValue = '') => {
  const [state, setState] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  const setLocalStorageState = newState => {
    try {
      const newStateValue =
        typeof newState === 'function' ? newState(state) : newState;
      setState(newStateValue);
      window.localStorage.setItem(key, JSON.stringify(newStateValue));
    } catch (error) {
      console.error(`Unable to store new value for ${key} in localStorage.`);
    }
  };

  return [state, setLocalStorageState];
};

export default useLocalStorage;
Enter fullscreen mode Exit fullscreen mode

This function updates both the React state and the corresponding key/value in localStorage. Note that we can also support functional updates like the regular useState hook.

And finally, we return the state value and our custom update function.

Now, we are good to go and can use the useLocalStorage hook to persist any data in our components in localStorage.

In the following example, we use it to store the application settings of the connected user.

import { useLocalStorage } from './hooks';

const defaultSettings = {
  notifications: 'weekly',
};

function App() {
  const [appSettings, setAppSettings] = useLocalStorage(
    'app-settings',
    defaultSettings
  );

  return (
    <div className="h-full w-full flex flex-col justify-center items-center">
      <div className="flex items-center mb-8">
        <p className="font-medium text-lg mr-4">Your application's settings:</p>

        <select
          value={appSettings.notifications}
          onChange={e =>
            setAppSettings(settings => ({
              ...settings,
              notifications: e.target.value,
            }))
          }
          className="border border-gray-900 rounded py-2 px-4 "
        >
          <option value="daily">daily</option>
          <option value="weekly">weekly</option>
          <option value="monthly">monthly</option>
        </select>
      </div>

      <button
        onClick={() => setAppSettings(defaultSettings)}
        className="rounded-md shadow-md py-2 px-6 bg-red-500 text-white uppercase font-medium tracking-wide text-sm leading-8"
      >
        Reset settings
      </button>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

useMediaQuery

Okay! Let's move on to our fourth React hook, useMediaQuery.

This hook will help us to test and monitor media queries programmatically inside our functional components. This is very useful, for example, when you need to render a different UI depending on the device's type or specific characteristics.

So our hook is accepting 3 arguments, which are:

  • first, the array of strings corresponding to media queries
  • then, an array of values matching those media queries, in the same order as the previous array
  • and finally, a default value if no media query matches
import { useState, useCallback, useEffect } from 'react';

const useMediaQuery = (queries = [], values = [], defaultValue) => {};

export default useMediaQuery;
Enter fullscreen mode Exit fullscreen mode

The first thing we do inside this hook is building a media query list for each matched media query. We gonna use this array to get the corresponding value by matching the media queries.

import { useState, useCallback, useEffect } from 'react';

const useMediaQuery = (queries = [], values = [], defaultValue) => {
  const mediaQueryList = queries.map(q => window.matchMedia(q));
};

export default useMediaQuery;
Enter fullscreen mode Exit fullscreen mode

And to that, we are creating a callback function wrapped inside the useCallback hook. We retrieve the value of the first media query in our list that matches or return the default value if none of them match.

import { useState, useCallback, useEffect } from 'react';

const useMediaQuery = (queries = [], values = [], defaultValue) => {
  const mediaQueryList = queries.map(q => window.matchMedia(q));

  const getValue = useCallback(() => {
    const index = mediaQueryList.findIndex(mql => mql.matches);
    return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
  }, [mediaQueryList, values, defaultValue]);
};

export default useMediaQuery;
Enter fullscreen mode Exit fullscreen mode

Then, we create a React state to store the matched value and initialize it using our function defined above.

import { useState, useCallback, useEffect } from 'react';

const useMediaQuery = (queries = [], values = [], defaultValue) => {
  const mediaQueryList = queries.map(q => window.matchMedia(q));

  const getValue = useCallback(() => {
    const index = mediaQueryList.findIndex(mql => mql.matches);
    return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
  }, [mediaQueryList, values, defaultValue]);

  const [value, setValue] = useState(getValue);
};

export default useMediaQuery;
Enter fullscreen mode Exit fullscreen mode

Finally, we add an event listener inside the useEffect hook to listen to each media query's changes. And we run the update function when changes occur.

Here we don't forget to clean up all those event listeners and return the state value from our hook.

import { useState, useCallback, useEffect } from 'react';

const useMediaQuery = (queries = [], values = [], defaultValue) => {
  const mediaQueryList = queries.map(q => window.matchMedia(q));

  const getValue = useCallback(() => {
    const index = mediaQueryList.findIndex(mql => mql.matches);
    return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
  }, [mediaQueryList, values, defaultValue]);

  const [value, setValue] = useState(getValue);

  useEffect(() => {
    const handler = () => setValue(getValue);
    mediaQueryList.forEach(mql => mql.addEventListener('change', handler));

    return () =>
      mediaQueryList.forEach(mql => mql.removeEventListener('change', handler));
  }, [getValue, mediaQueryList]);

  return value;
};

export default useMediaQuery;
Enter fullscreen mode Exit fullscreen mode

A simple example I've used recently is to add a media query to check if the device allows the user to hover over elements. This way, I could add specific opacity styles if the user can hover or apply basic styles otherwise.

import { useMediaQuery } from './hooks';

function App() {
  const canHover = useMediaQuery(
    // Media queries
    ['(hover: hover)'],
    // Values corresponding to the above media queries by array index
    [true],
    // Default value
    false
  );

  const canHoverClass = 'opacity-0 hover:opacity-100 transition-opacity';
  const defaultClass = 'opacity-100';

  return (
    <div className={canHover ? canHoverClass : defaultClass}>Hover me!</div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

useDarkMode

Okay, guys! One more hook to go.

This one is my favorite. It allows me to easily and quickly apply the dark mode feature to any of my React applications.

AlterClass - Dark Mode with custom React hook

Let's see how to build such a hook.

This hook aims to enable and disable the dark mode on-demand, store the current state in localStorage.

For that, we are going to use two of the hooks we just built: useMediaQuery and useLocalStorage.

With useMediaQuery, we can check the user's browser preference for dark mode.

Then, with 'useLocalStorage,' we can initialize, store, and persist the current state (dark or light mode) in localStorage.

import { useEffect } from 'react';
import useMediaQuery from './useMediaQuery';
import useLocalStorage from './useLocalStorage';

const useDarkMode = () => {
  const preferDarkMode = useMediaQuery(
    ['(prefers-color-scheme: dark)'],
    [true],
    false
  );
};

export default useDarkMode;
Enter fullscreen mode Exit fullscreen mode

Finally, the final piece of this hook is to fire a side effect to add or remove the dark class to the *document.body* element. This way, we could simply apply dark styles to our application.

import { useEffect } from 'react';
import useMediaQuery from './useMediaQuery';
import useLocalStorage from './useLocalStorage';

const useDarkMode = () => {
  const preferDarkMode = useMediaQuery(
    ['(prefers-color-scheme: dark)'],
    [true],
    false
  );

  const [enabled, setEnabled] = useLocalStorage('dark-mode', preferDarkMode);

  useEffect(() => {
    if (enabled) {
      document.body.classList.add('dark');
    } else {
      document.body.classList.remove('dark');
    }
  }, [enabled]);

  return [enabled, setEnabled];
};

export default useDarkMode;
Enter fullscreen mode Exit fullscreen mode

And if you are looking for an easy way to do it, once again, have a look at Tailwind CSS, which supports dark mode. Coupled with this hook, Tailwind CSS becomes the easiest and fastest way of implementing dark mode in any React applications.

Conclusion

Alright! That's it, guys. Thank you so much for watching (or reading this article).

I really hope that this video was useful for you. Make sure to check the Github repository to get the source code of all the hooks we just built together.

Please share this video with your friends, hit the like button, and don't forget to subscribe on YouTube.

Become a React Developer

And if you need to learn more about building modern web applications with React, go check out my course on AlterClass.io.

AlterClass.io

My course will teach you everything you need to master React, become a successful React developer, and get hired!

AlterClass.io

I'll teach all the concepts you need to work with React, you'll get tons of hands-on practice through quizzes and programming assessments, and you'll build real-world projects on your own.

AlterClass.io

AlterClass.io

Plus, you'll be part of a growing community of learners.

So go to AlterClass.io, enroll in my course, and start building an amazing portfolio of powerful React applications.

Top comments (6)

Collapse
 
puruvj profile image
PuruVJ

Nice article.

I also have a useTheme hooks which is very similar to your useDarkMode hook

import { atom, useAtom } from 'jotai';
import { useEffect, useLayoutEffect } from 'react';

export type TTheme = 'light' | 'dark';

const themeAtom = atom<TTheme>('light');

export function useTheme() {
  // Media query
  const systemTheme: TTheme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  const localValue = localStorage.getItem('theme:type') as TTheme;

  const [theme, setTheme] = useAtom(themeAtom);

  useEffect(() => {
    setTheme(localValue || systemTheme);
  }, []);

  useLayoutEffect(() => {
    localStorage.setItem('theme:type', theme);

    document.body.dataset.theme = theme;
  }, [theme]);

  return [theme, setTheme] as const;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
anilsansak profile image
Yaşar Anıl Sansak • Edited

Great article! However, in the explanation of the useFetch custom hook, I think that there is a mistake in the third code snippet. You used isMounted variable in a if check, but it is not defined anywhere else. Either I am not seeing it or you have forgot to initialize it first.

thanks :)

Collapse
 
dastasoft profile image
dastasoft

Great article, thanks for posting!

For the useFetch hook I would recommend using a useReducer because the state is close related in each action and instead of having flags for the loading part, doing a state machine, check this article about it for me was a game changer: kentcdodds.com/blog/stop-using-isl...

Collapse
 
qq449245884 profile image
qq449245884

Dear AlterClass,may I translate your article into Chinese?I would like to share it with more developers in China. I will give the original author and original source.

Collapse
 
developergp profile image
GPWebDeveloper

Very good. Thanks

Collapse
 
kieran6roberts profile image
Kieran Roberts

Nice article thanks! I have also found myself reaching for these kinds of hooks regularly in my time using react.