Building Custom React Hooks: A Practical Guide

Building Custom React Hooks: A Practical Guide

Custom hooks are one of React’s most powerful features, allowing you to extract component logic into reusable functions. Let’s explore how to build practical custom hooks.

What Are Custom Hooks?

Custom hooks are JavaScript functions that start with “use” and can call other hooks. They let you share stateful logic between components without adding more components to your tree.

Example 1: useLocalStorage Hook

This hook synchronizes state with localStorage:

import { useState, useEffect } from "react";

function useLocalStorage(key, initialValue) {
  // Get initial value from localStorage or use provided initial value
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  // Update localStorage when value changes
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error(error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue];
}

// Usage
function App() {
  const [name, setName] = useLocalStorage("name", "Guest");

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <p>Hello, {name}!</p>
    </div>
  );
}

Example 2: useFetch Hook

A custom hook for data fetching:

import { useState, useEffect } from "react";

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

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

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);

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

  return <div>{data.name}</div>;
}

Example 3: useDebounce Hook

Debounce values to optimize performance:

import { useState, useEffect } from "react";

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

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

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

  return debouncedValue;
}

// Usage in a search component
function SearchBar() {
  const [searchTerm, setSearchTerm] = useState("");
  const debouncedSearch = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearch) {
      // Perform search API call
      console.log("Searching for:", debouncedSearch);
    }
  }, [debouncedSearch]);

  return (
    <input
      type="text"
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="Search..."
    />
  );
}

Best Practices

  1. Always start with “use” - This tells React and linters that it’s a hook
  2. Keep hooks focused - Each hook should do one thing well
  3. Document dependencies - Make sure useEffect dependencies are complete
  4. Handle cleanup - Return cleanup functions when necessary

Conclusion

Custom hooks are essential for writing clean, reusable React code. They help you avoid code duplication and make your components simpler and easier to test.