DEV Community

Cover image for A Beginner's Guide to React Custom Hooks
Olaleye Blessing
Olaleye Blessing

Posted on

A Beginner's Guide to React Custom Hooks

React hooks have been a game changer for developers since they were introduced in React 16.8. Hooks give developers access to state and other React features without using class components. This tutorial will discuss how to create custom React hooks.

Prerequisites

To understand this tutorial, you should have a basic understanding of React, JavaScript, and ES6 syntax. It's also recommended that you have some experience using hooks.

A Quick Overview of React Hooks

Before hooks came along, state and other React features could only be used in class components. With hooks, you can create reusable stateful logic that can be shared across multiple components. Some of the commonly used React hooks are useState, useEffect, useContext, and useRef.

Rules Guiding Usage of React Hooks

There are a couple of rules that should be followed when creating and using hooks:

  • Names of hooks must start with "use" to indicate that it is a hook.
 const useMyHook = () => {/*....*/}

 const useAnotherHook = () => {/*....*/}
Enter fullscreen mode Exit fullscreen mode
  • Hooks should only be used at the top level of a functional component or another hook. They should not be used inside loops, conditions, or nested functions as they can lead to unexpected behavior.
  const MyComponent = () => {
      const items = [];

      if(items.length > 2) {
        useMyHook(); // do not do this
      }

      items.forEach(item => useMyHook()); // do not do this

      return <p>My component</p>
  }
Enter fullscreen mode Exit fullscreen mode
  • Hooks must only be called from within a functional component or another hook. They cannot be called from regular JavaScript functions.
 const myFunction = () => {
   const hookResult = useMyHook(); // wrong!
 }

 const useMyHook = () => {
   const other = useMyOtherHook(); // correct!
   // .............
   // .............
 }

 const MyComponent = () => {
   const myHook = useMyHook();

   return <p>My Component</p>
 }   
Enter fullscreen mode Exit fullscreen mode

Why Create Custom Hooks

Creating custom hooks is useful for quite a number of reasons:

  • Custom hooks allow us to extract and reuse stateful logic in our React components. By encapsulating stateful logic in a custom hook, we can make our code more modular and easier to maintain.
  • Custom hooks allow us to separate concerns in our code, making it easier to read and test.

Creating a Custom Hook

Now that we have a reason to use custom hooks, let’s create three hooks and see them in action:

  1. useWindowWidth
  2. useLocalStorage
  3. useFetch

💡 Note: You can find the code for each hook in the sandbox.

useWindowWidth

useWindowWidth allows us to get the current width of the window. It uses the resize event to update the window's width whenever the window is resized:

// /src/hooks/useWindowWidth.js
import { useState, useEffect } from "react";

// notice that the hook name starts with `use`
export const useWindowWidth = () => {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
    };

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return { windowWidth };
};

Enter fullscreen mode Exit fullscreen mode

With this hook present, we don’t have to write the logic for resizing the window every time we want to get the window’s width in a component.

Also notice that we are returning a value. It’s common but not compulsory to return any JavaScript data type from a hook. We are returning the windowWidth in an object so that it will be easily destructured.

We will call our hook at the top level of any component that wants to use it:

// /src/components/Window.jsx
import { useWindowWidth } from "./../hooks/useWindowWidth";

const Window = () => {
  const { windowWidth } = useWindowWidth();

  return (
    <section>
      <h1>Window width:</h1>
      <p>{windowWidth}</p>
    </section>
  );
};

export default Window;

Enter fullscreen mode Exit fullscreen mode

useLocalStorage

useLocalStorage allows us to store data in the browser’s local storage and retrieve it in a different component:

// /src/hooks/useWindowWidth.js
import { useState } from "react";

export const useLocalStorage = (key, initialValue) => {
  const [value, setValue] = useState(() => {
    const localVal = localStorage.getItem(key);

    return localVal === null ? initialValue : JSON.parse(localVal);
  });

  const updateValue = (newValue) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  };

  return { value, updateValue };
};

Enter fullscreen mode Exit fullscreen mode

Just like normal functions, custom hooks can also accept any numbers of arguments. We passed in the key and an initial value in this case:

  • key represents the local storage key.
  • initialValue represents a default value to use if there is no value for the key in the local storage.

Notice we are returning a value and function this time around. The updateValue can be called just like every other JavaScript function.

We can then use our hook like this:

// /src/components/Stack.jsx
import { useLocalStorage } from "./../hooks/useLocalStorage";

const Stack = () => {
  const { value: stack, updateValue: updateStack } = useLocalStorage(
    "stack",
    "JS"
  );

  return (
    <section>
      <h1> Stack: </h1>
      <p>{stack}</p>
      <button onClick={() => updateStack(stack === "JS" ? "TS" : "JS")}>
        Update stack
      </button>
    </section>
  );
};

Enter fullscreen mode Exit fullscreen mode

useFetch

useFetch fetches data asynchronously. It returns these states:

  • loading determines if the request is still processing.
  • data represents the resource we requested.
  • error represents an error message.
// /src/hooks/useFetch.js
import { useEffect, useState } from "react";

export const useFetch = (url) => {
  const [resource, setResource] = useState({
    loading: true,
    error: null,
    data: null
  });

  useEffect(() => {
    const fetchData = async () => {
      try {
        const req = await fetch(url);
        const data = await req.json();
        setResource((prev) => ({ ...prev, data }));
      } catch (error) {
        setResource((prev) => ({ ...prev, error: error.message }));
      } finally {
        setResource((prev) => ({ ...prev, loading: false }));
      }
    };

    fetchData();
  }, []);

  return resource;
};

Enter fullscreen mode Exit fullscreen mode

💡 Note: This useFetch isn't production ready.

We can then call our hook in any component like this:

// /src/components/Posts.jsx
import { useFetch } from "./../hooks/useFetch";

const postsUrl = "<https://jsonplaceholder.typicode.com/posts>";

const Posts = () => {
  const { data, loading, error } = useFetch(postsUrl);

  if (loading) return <div>Loading</div>;

  if (error) return <div>Error</div>;

  const posts = [...data].slice(1, 10);

  return (
    <section>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.body}</li>
        ))}
      </ul>
    </section>
  );
};

export default Posts;

Enter fullscreen mode Exit fullscreen mode

Custom Hook Share Stateful Logic, not State itself

Custom hooks are meant to share stateful logic, and not state itself. Two different components using the same custom hook are not sharing the same state that the custom hook provides. This means that you should not use a custom hook to share the actual state between components. When you need to share the state itself between multiple components, lift it up to a parent and pass it down instead.

Let's look at our useLocalStorage for an example. Assume that we have another component, Profile, that calls the hook like this:

// /src/components/Profile.jsx
import { useLocalStorage } from "../hooks/useLocalStorage";

const Profile = () => {
  const { value: gender, updateValue: updateGender } = useLocalStorage(
    "gender",
    "Male"
  );

  return (
    <section>
      <h1> Gender: </h1>
      <p>{gender}</p>
      <button
        onClick={() => updateGender(gender === "Male" ? "Female" : "Male")}
      >
        Update gender
      </button>
    </section>
  );
};

export default Profile;

Enter fullscreen mode Exit fullscreen mode

The Stack and Profile components are not sharing the same state even though they are calling the useLocalStorage hook. Updating our stack with the function returned by useLocalStorage won't update the gender, likewise updating the gender won't update the stack.

Conclusion

Custom React hooks are a powerful tool for creating reusable stateful logic in your React applications. They allow you to separate concerns and make your code more modular and easier to maintain. By following the rules for using hooks and starting with simple examples, you can create custom hooks that are easy to use.

You can read more about custom hooks from the react documentation.

Happy coding.

Top comments (0)