DEV Community

Cover image for React Custom Hooks: Crafting Reusable and Clean Code Like a Pro
Ibukunoluwa Popoola
Ibukunoluwa Popoola

Posted on

React Custom Hooks: Crafting Reusable and Clean Code Like a Pro

In the world of React, hooks have revolutionized how we manage state and side effects in functional components. However, as applications become complex, we often find ourselves repeating logic across different components. This is where custom hooks come in handy. Custom hooks allow us to extract reusable logic, making our components cleaner and more maintainable.

In this post, we'll explore how to create custom hooks and demonstrate a real-world example that enhances code reusability and abstraction. To get the most out of this post, it's beneficial if you're already familiar with the basics of React, including functional components, the use of built-in hooks like useState and useEffect, and fundamental JavaScript concepts such as Promises and asynchronous operations. If you're comfortable with these concepts, you'll find it easier to follow along and implement custom hooks in your projects.

Custom hooks enable you to:

  1. Encapsulate Logic: Separate logic from UI components, making code more modular and easier to manage.
  2. Reuse Logic: Share common logic across multiple components, reducing duplication.
  3. Simplify Components: Keep components focused on rendering UI by offloading business logic to hooks.

Creating a Custom Hook: A Practical Example

Let's consider a common use case: fetching and managing data from an API. We'll create a custom hook, useFetch, that handles data fetching, loading states, and errors.

Step 1: Setup the Basic Hook Structure

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
};

export default useFetch;

Enter fullscreen mode Exit fullscreen mode

Step 2: Using the Custom Hook in a Component

Now that we have our custom useFetch hook, let's use it in a component.

import React from 'react';
import useFetch from './useFetch';

const UserList = () => {
  const { data: users, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users');

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

export default UserList;

Enter fullscreen mode Exit fullscreen mode

Step 3: Enhancing the Custom Hook

To make our useFetch hook more versatile, we'll add a feature that allows re-fetching data on demand. This can be useful in scenarios where you need to refresh the data in response to user actions or other events.

To achieve this, we will refactor the fetchData function out of the useEffect hook and wrap it in a useCallback hook. This approach serves two purposes:

  1. Reusability: By extracting fetchData, we can call it directly from components using the hook, allowing us to re-fetch data as needed.

  2. Memory Optimization: Wrapping fetchData in useCallback ensures that the function instance remains stable across re-renders unless the dependencies change. This prevents unnecessary re-creation of the function, which can help optimize memory usage and reduce the chances of re-triggering the effect unintentionally.

Here’s the updated implementation:

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

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

  const fetchData = useCallback(async (abortController) => {
    setLoading(true);
    try {
      const response = await fetch(url, {
        signal: abortController.signal
      });
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      const result = await response.json();
      setData(result);
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('Request was cancelled');
      } else {
        setError(error.message);
      }
    } finally {
      setLoading(false);
    }
  }, [url]);

  useEffect(() => {
    // AbortController is used to abort ongoing fetch requests when the component unmounts or the URL changes
    const abortController = new AbortController();

    fetchData(abortController);

    // Cleanup function to cancel the request when the component unmounts or the URL changes
    return () => {
      abortController.abort();
    };
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
};

export default useFetch;

Enter fullscreen mode Exit fullscreen mode

Now, the useFetch hook provides a refetch function that can be called to re-fetch the data.

Other Use Cases Where Custom Hooks Are Ideal

Here are 3 other common use cases where custom hooks can be particularly beneficial:

  • useLocalStorage: Managing state that persists across sessions can be challenging. The useLocalStorage custom hook can simplify this by abstracting the logic for storing and retrieving values from the localStorage API. It provides a way to keep the component state in sync with local storage, ensuring that data is saved even if the user closes or refreshes the browser.

Example Implementation:

// https://usehooks.com/useLocalStorage
import { useState } from 'react';

// Hook
function useLocalStorage(key, initialValue) {
  // State to store our value
  // Pass initial state function to useState so logic is only executed once
  const [storedValue, setStoredValue] = useState(() => {
    if (typeof window === 'undefined') {
      return initialValue;
    }

    try {
      // Get from local storage by key
      const item = window.localStorage.getItem(key);
      // Parse stored JSON or if none return initialValue
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      // If error also return initialValue
      console.log(error);
      return initialValue;
    }
  });

  // Return a wrapped version of useState's setter function that ...
  // ... persists the new value to localStorage.
  const setValue = (value) => {
    try {
      // Allow value to be a function so we have the same API as useState
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      // Save state
      setStoredValue(valueToStore);
      // Save to local storage
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      // A more advanced implementation would handle the error case
      console.log(error);
    }
  };

  return [storedValue, setValue];
}

Enter fullscreen mode Exit fullscreen mode
  • useWindowSize: Handling responsive design often requires tracking the window size to adjust layouts or elements dynamically. The useWindowSize hook abstracts the logic for detecting and reacting to window resize events, making it easy to implement responsive UI elements.

Example Implementation:

import { useState, useEffect } from 'react';

const useWindowSize = () => {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
};

Enter fullscreen mode Exit fullscreen mode
  • useDebounce: The useDebounce hook is useful for delaying the execution of a function after a certain period, typically to avoid calling an expensive operation multiple times in quick succession. This is particularly useful for handling search inputs, where you want to wait until the user stops typing before making an API call.

Example Implementation:

import { useState, useEffect } from 'react';

const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
};

// Use in a component
import { useDebounce } from './useDebounce';

const SearchComponent = () => {
   const [searchTerm, setSearchTerm] = useState('')
   const debouncedSearchTerm = useDebounce(searchTerm, 500)  // Will only trigger API call 500ms after user stops typing!

   useEffect(() => {
      if(debouncedSearchTerm) {
         // Make API call!
      }
   }, [debouncedSearchTerm])

   return <input onChange={e => setSearchTerm(e.target.value)} />
}
Enter fullscreen mode Exit fullscreen mode

Each of these custom hooks addresses a specific problem and can significantly simplify your code by abstracting common logic into reusable components. This not only reduces duplication but also enhances the clarity and maintainability of your React applications.

Best Practices for Custom Hooks

  1. Use the use prefix: Ensure your custom hook's name starts with use to follow React's conventions and enable the hook rules.
  2. Keep hooks focused: Custom hooks should do one thing well. Avoid overloading them with too much logic.
  3. Handle cleanup: Use the useEffect cleanup function to handle any necessary cleanup operations, like cancelling network requests.

Conclusion

Custom hooks are a powerful tool in React that can help you write cleaner, more maintainable code. By encapsulating logic into reusable hooks, you can keep your components focused on rendering UI and improve your application's scalability.

Feel free to use and extend the hooks outlined above, and try creating custom hooks in your next project, and see how they can simplify your codebase! Check out useHooks for many other useful hooks. Make sure to keep an eye out on React 19 as well. The 2024 conference happened recently and the new hooks are off the hook. Till next time, useTime...

Top comments (2)

Collapse
 
olutola_aa profile image
Olutola

The conclusion πŸ˜‚πŸ˜‚

Collapse
 
skygdi profile image
Peter George

Thanks bro!!