DEV Community

Cover image for Demystifying Custom Hooks in React
Sylvester Olawale
Sylvester Olawale

Posted on

Demystifying Custom Hooks in React

In the constantly evolving and ever-dynamic world of web development, React has remained a leading frontrunner, offering an incredibly declarative and remarkably efficient approach to the art and science of building user interfaces.

Next.js, a widely-used, supercharged and lightning-fast framework that is built atop React, continues to heavily rely on the very best of its foundational features. It's absolutely imperative, then, that these fundamental building blocks of React itself remain thoroughly discussed and deeply understood, especially by those who are just beginning their journey into the vast and intricate world of web development.

Among the vast arsenal of features that React boasts, React Hooks notably stand out for their unique ability to let developers use state and other React features without the cumbersome necessity of writing a class. However, the concept of custom hooks often remains wrapped in a veil of mystery, with a significant number of developers either misunderstanding how they're formed or their true purpose and as such not fully leveraging their power.

This post aims to demystify the enigmatic world of custom hooks, thoroughly guiding you through their creation and use, with a special focus on the useState and useEffect hooks. We will culminate in building a practical and illustrative example: the usePosts hook for fetching data from a public API, we then discuss some hook rules and briefly touch on some of the other commonly used hooks, demonstrating how these concepts, which are the very cornerstone of React itself, must remain discussed and understood, by new comers and experts alike in the world of React.

What are Custom Hooks?

A custom hook is simply a JavaScript function that starts with use and allows you to encapsulate and reuse logic across multiple React components by leveraging React's built-in hooks, such as useState and useEffect, without duplicating code.

      const {posts, isLoading, error} = usePosts()
Enter fullscreen mode Exit fullscreen mode

What are React Hooks?

React Hooks were introduced in React 16.8, revolutionizing how developers work with React by allowing the use of state and other React features in functional components. Hooks offer a more direct API to the React concepts you already know: props, state, context, refs, and lifecycle.

React Conf 2018

simply put, a react hook helps you hook into or opt into React state life and lifecycle features from function components without writing a class. There are very commonly used ones like useState and useEffect provided to us as APIs to use from react itself

The Cornerstones: useState and useEffect

Before diving into custom hooks, let's understand the two pillars that most React developers start with:

useState and useEffect.

  • useState Hook: This hook is a staple for adding state to functional components. It lets you add React state to your components in a classless fashion. With useState, you can track and update your component's local state, making it reactive to user inputs or other changes.

When you use useState, you typically destructure it into two parts: the current state value and a function to update this value. Here's the basic syntax:

     const [state, setState] = useState(initialState);
Enter fullscreen mode Exit fullscreen mode

Anatomy of a useState Hook

  • initialState: This is the value you want your state to be initialized with. It can be any data type — a number, string, boolean, object, or even an array. This value is only used the first time the component renders.

  • state: This represents the current state value. It's what you use to read the state within your component.

  • setState: This is a function that allows you to update the state. When you call this function with a new value, React will re-render the component with the updated state.

What's going on?

  • Initialization: Upon the initial render of the component, useState sets up the react state using the initialState value you provide. This setup happens only once.

  • Reading State: Whenever you need to display or otherwise use the state in your component, you simply reference the state variable.

  • Updating State: When you need to change the value of the state, you call setState with the new value. This triggers a component re-render, updating the UI with the new state.

  • Re-rendering: After setState is called, React schedules a re-render of the component. During this re-render, the component uses the updated state value. This is how the UI stays in sync with the state.

import React, { useState } from 'react';
import axios from 'axios';
import LoadingSpinner from './ui/LoadingSpinner'

export function PostPage() {

const [isLoading, setIsLoading] = useState(false) 

const url = '/api-url.com'

const fetchSomeData = async() => {
      setIsLoading(true); //setState to true
      const response = await axios.get(url);
      const data = response.data
      console.log(data, 'data');
      setIsLoading(false); //setState to false
};

if (isLoading) { //use appState
  return <LoadingSpinner /> //fallback component
  }

return (
  <div>
    <Some Component />
  </div>
)

}
Enter fullscreen mode Exit fullscreen mode

In the example provided, when the component initially loads, we set the isLoading state to false, indicating there are no ongoing data fetching operations. As we initiate an asynchronous data fetch — a process that might delay due to network latency or server response times — we promptly update isLoading to true. This change triggers our UI to display a visual cue, such as a spinner or a "Loading..." text, alerting users to an active background operation. Upon completion of the data fetching, regardless of success or failure, we revert isLoading to false to remove the visual cue and signal to the users that the loading has ceased.

However, the fetch operation in our example is overly simplistic and does not fully address potential failures, highlighting the necessity for implementing error handling strategies to gracefully manage and notify the app user of any issues during the data fetch process.

To ensure that this asynchronous fetch operation seamlessly integrates without hindering other synchronous tasks, we would execute it as a side effect within the component, facilitated by the useEffect hook. This approach guarantees that the fetch operation is managed efficiently, adhering to the component's lifecycle and not obstructing the user experience with unnecessary delays or blockages.

  • useEffect Hook: The useEffect hook is essential for performing side effects in your components. Whether you need to fetch data, set up a subscription, or manually change the DOM in React components, useEffect serves your needs. It effectively replaces lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount.
export function PostPage() {
 const [posts, setPosts] = useState([]);
 const [isLoading, setIsLoading] = useState(true);

 const url = 'https://jsonplaceholder.typicode.com/posts';

 const fetchPosts = async () => {
   try {
      setIsLoading(true);
      const response = await axios.get(url);
      const data = response.data
      setUser(data); // Update the state with the fetched data
      setIsLoading(false);
    } catch (error) {
      console.log(error);
    }
 };

 useEffect(() => {
     // call the async function inside the effect
    fetchData();
  }, []); 

// The empty array means this effect runs once after the initial render

  if (isLoading) {
    return <LoadingSpinner /> 
    }

  return (
     <div>
      {posts.map((post) => {
        const { id, title, body } = post;
        return (
          <ul key={id}>
            <li>{title}</li>
            <p>{body}</p>
          </ul>
        );
      })}
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example, useEffect is used to perform an asynchronous data fetch operation. The empty dependency array ([]) passed to useEffect ensures the effect only runs once after the initial render, similar to componentDidMount in class components. Once the data is fetched, it updates the user state with the fetched data, triggering a re-render to display the user information.

It's also important to mention that the asynchronous function fetchPosts() could have been defined directly within the useEffect hook. However, we opted to separate the function from the useEffect definition to delineate concerns more clearly and to allow for potential reuse of fetchPosts() outside the context of this specific effect.

By structuring the code in this manner, we enhance the modularity and readability of our component. This separation ensures that fetchPosts() can be easily called in different contexts or in response to various events, not just as a part of the component's initial effect

Now we've seen the useState hook that helps us initialize a state and update state based on certain conditions, as well as the useEffect hook for executing side effects such as data fetching...

we now turn our attention towards the concept of custom hooks which was previously defined as...

a JavaScript function that starts with use and allows you to encapsulate and reuse logic across multiple React components.

Building on this foundation, we're going to extract the data fetching logic we've developed into a custom hook called usePosts, ensuring that if the same data fetching logic is required elsewhere within the app, we can easily reuse it without duplicating the entire set of operations. This practice of creating and utilizing custom hooks mirrors the philosophy behind reusable components in React, emphasizing clean, comprehensible, and highly maintainable code.

Creating the usePosts Hook

Let's create a custom hook, usePosts.

In essence, what we're just doing is extracting the logic we already have into a hook.

But let's define our logic again.

To fetch data from the JSONPlaceholder API and expose the relevant data for consumption in our components we need to:

  • Set Up State: First, we use the useState hook to set up the state for storing our posts and loading status.

  • Fetch Data: Next, we utilize the useEffect hook to handle the data fetching. Inside useEffect, we make an asynchronous call to the API, update our posts state with the fetched data, and adjust the loading status accordingly.

  • Expose Data: Finally, we return the fetched posts and the loading status from our custom hook. This allows any component that calls usePosts to easily access the list of posts and the loading state.

import { useState, useEffect } from 'react';

export function usePosts() {
 const [posts, setPosts] = useState([]);
 const [isLoading, setIsLoading] = useState(true);
 const [error, setError] = useState(null);

 const url = 'https://jsonplaceholder.typicode.com/posts';

 const fetchPosts = async () => {
   try {
      setIsLoading(true);
      const response = await axios.get(url);
      const data = response.data
      setPosts(data); 
      setIsLoading(false);
    } catch (error) {
      setError(error); 
      setIsLoading(false);
    }
 };

 useEffect(() => {
    fetchPosts();
  }, []); 

  return { posts, isLoading, error };

 // We return the data and states that might be needed by components using this hook,
// allowing them to easily access the current posts, loading status, and any errors.
}

Enter fullscreen mode Exit fullscreen mode

For creating hooks, it's crucial to adhere to the convention of prefixing the hook name with use. This naming convention signals to React and to developers that we're dealing with a custom hook, following the pattern established by React's own hooks (e.g., useState, useEffect). This not only aligns with React's design principles but also ensures clarity and consistency in code

With our usePosts custom hook now ready, integrating it into our components is remarkably straightforward. Here's how you can leverage usePosts within a functional component to fetch and display data:

import { usePosts } from './hooks/usePosts';

function PostPage() {

    // Destructure the necessary states from the usePosts hook. This way, the component
   // can directly use the posts data, loading status, and error information without
  // managing the fetch logic itself, leading to cleaner and more maintainable code.

  const { posts, isLoading, error } = usePosts();

   if (isLoading) {
    return <LoadingSpinner /> 
    }
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {posts.map((post) => {
        const { id, title, body } = post;
        return (
          <ul key={id}>
            <li>{title}</li>
            <p>{body}</p>
          </ul>
        );
      })}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Compared to the previous approach where data fetching logic was directly embedded within the post component, this refined strategy, utilizing the usePosts custom hook, significantly enhances code cleanliness and reusability.

It aligns perfectly with the fundamental coding principle of maintaining clean, modular, and reusable code. By extracting the fetching logic into a custom hook, we simplify the component's structure, making it more readable and easier to maintain, while also facilitating the reuse of the data fetching logic across different parts of the application without duplication.

Before we wrap up our discussion on custom hooks, it's crucial to touch upon the rules that govern their use in React. Adhering to these rules ensures that your hooks work as intended, without unexpected behavior.

Hook Rules

Only Call Hooks at the Top Level:

Avoid calling hooks inside loops, conditions, or nested functions. This rule ensures that hooks are called in the same order each time a component renders.

Calling Hooks Inside Loops (Violation)

Attempting to use a hook inside a loop disrupts the consistent order of hook calls between renders.

import React, { useState } from 'react';

function MyComponent({ items }) {
  for (let item of items) {
    // ❌ Violation: This hook call is inside a loop.
    const [isActive, setIsActive] = useState(false);

    // Logic to toggle isActive for each item...
  }

  // Rest of component...
}
Enter fullscreen mode Exit fullscreen mode
Calling Hooks Inside Conditions (Violation)

Conditional execution of hooks can also lead to inconsistencies in the order of hook calls.

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

function MyComponent({ isFeatureEnabled }) {
  if (isFeatureEnabled) {
    // ❌ Violation: This hook call is inside a condition.
    useEffect(() => {
      // Logic specific to when the feature is enabled...
    }, []);
  }

  // Rest of component...
}

Enter fullscreen mode Exit fullscreen mode
Calling Hooks Inside Nested Functions (Violation)

Hooks should not be called from within nested functions, as it breaks the rule of hooks being called at the top level.

import React, { useState } from 'react';

function MyComponent() {
  function handleClick() {
    // ❌ Violation: This hook call is inside a nested function.
    const [clicks, setClicks] = useState(0);

    // Logic to handle clicks...
  }

  // Rest of component...
}

Enter fullscreen mode Exit fullscreen mode

Only Call Hooks from React Functions:

You should only call hooks from React functional components or from custom hooks. This use ensures the hooks' proper functioning within React's state and lifecycle mechanisms.

Calling Hooks Outside of React Functions (Violation)

Hooks must be called within React functional components or custom hooks, not in regular JavaScript functions.

import { useState } from 'react';

// ❌ Violation: This is not a React function component or a custom hook.
function regularFunction() {
  const [value, setValue] = useState('');

  // Logic involving the state...
}

Enter fullscreen mode Exit fullscreen mode

To avoid these errors, ensure that hooks are always used at the top level of your React functional components or custom hooks, without being enclosed in loops, conditions, or nested functions. Additionally, hooks should only be called from within React function components or other custom hooks. By adhering to these rules, you maintain the integrity and predictability of your component's state and lifecycle behavior.

Beyond useState and useEffect

React provides several built-in hooks, each designed for different aspects of managing state and side effects in your application. Here's a look at some of the most commonly used hooks aside from useState and useEffect, along with brief code examples of their usage.

useContext: Allows you to access the value of a React context provided by a component higher up in the component tree without having to pass the context down through props.

useRef: Returns a mutable ref object whose .current property is initialized to the passed argument. It can be used to access a DOM element directly or store any mutable value that doesn't cause a re-render when updated.

useReducer: An alternative to useState for managing complex state logic in components. It lets you manage local component state with a reducer function.

useMemo: Returns a memoized value. Use this hook to optimize performance by memoizing expensive calculations if the dependencies haven't changed.

useCallback: Returns a memoized callback function. This hook is useful for passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.

useImperativeHandle: Customizes the instance value that is exposed to parent components when using ref. This hook should be used with forwardRef.

useLayoutEffect: Similar to useEffect, but it fires synchronously after all DOM mutations. Use this hook to read layout from the DOM and re-render synchronously.

useDebugValue: Can be used to display a label for custom hooks in React DevTools. It doesn’t impact the behavior of your code but can help you improve the debuggability of your custom hooks.

I plan to dedicate a separate blog post to delve into some of the more intricate hooks provided by React, specifically focusing on useContext, useRef, useReducer, and useMemo. These hooks possess powerful features that are widely utilized in modern React applications. By exploring each of these hooks in detail, we'd shed light on their unique capabilities and demonstrate how they can be effectively employed to enhance the functionality and performance of React applications.

Thank you for taking the time to read this! I sincerely hope you found the information valuable. I encourage you to share your thoughts and insights in the comments section below. Let's keep the flame of curiosity burning bright 😃. Happy coding, and here's to pushing the boundaries of what we can achieve with React! 🚀

follow me on twitter: waliba.eth

Top comments (0)