React Custom Hooks

Creating reusable logic with custom hooks

🎣 What are Custom Hooks?

Custom Hooks are JavaScript functions that let you extract and reuse stateful logic between components. They start with "use" and can call other hooks inside them.


import { useState, useEffect } from 'react';

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return width;
}
                                    

Custom Hook Concepts

♻️

Reusability

Share logic across multiple components

const data = useCustomHook();
📦

Encapsulation

Hide complex logic in a single function

function useAuth() { /* logic */ }
🔧

Composition

Combine multiple hooks together

function useData() {
  const [state] = useState();
  useEffect(() => {});
}

Naming Convention

Must start with "use"

function useFetch() { }

🔹 Simple Custom Hook Example

Create a custom hook to manage form input state. This hook encapsulates the common pattern of handling input changes and can be reused across different forms.

import { useState } from 'react';

// Custom Hook
function useInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  const handleChange = (e) => {
    setValue(e.target.value);
  };

  const reset = () => {
    setValue(initialValue);
  };

  return { value, onChange: handleChange, reset };
}

// Using the Custom Hook
function LoginForm() {
  const email = useInput('');
  const password = useInput('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Email:', email.value);
    console.log('Password:', password.value);
    email.reset();
    password.reset();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input {...email} type="email" placeholder="Email" />
      <input {...password} type="password" placeholder="Password" />
      <button type="submit">Login</button>
    </form>
  );
}

Output:

🔹 useFetch Custom Hook

A practical custom hook for data fetching. This hook handles loading states, errors, and data management, making API calls simpler and more consistent across your app.

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('Failed to fetch');
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        setError(err.message);
        setData(null);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

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

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

  return (
    <div>
      <h2>{data.name}</h2>
      <p>{data.email}</p>
    </div>
  );
}

Output:

🔹 useLocalStorage Custom Hook

Sync state with localStorage automatically. This hook persists data across page refreshes and provides a simple API similar to useState.

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // Get initial value from localStorage or use provided initial value
  const [value, setValue] = 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(value));
    } catch (error) {
      console.error(error);
    }
  }, [key, value]);

  return [value, setValue];
}

// Using the Custom Hook
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </div>
  );
}

🔹 useToggle Custom Hook

Simplify boolean state management with a toggle hook. Perfect for modals, dropdowns, and any on/off state.

import { useState } from 'react';

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = () => setValue(prev => !prev);
  const setTrue = () => setValue(true);
  const setFalse = () => setValue(false);

  return { value, toggle, setTrue, setFalse };
}

// Using the Custom Hook
function Modal() {
  const modal = useToggle(false);

  return (
    <div>
      <button onClick={modal.setTrue}>Open Modal</button>
      
      {modal.value && (
        <div className="modal">
          <h2>Modal Content</h2>
          <button onClick={modal.setFalse}>Close</button>
        </div>
      )}
    </div>
  );
}

Output:

🔹 Custom Hook Best Practices

Follow these guidelines when creating custom hooks:

✅ Do:

  • Start with "use": Required naming convention (useMyHook)
  • Extract reusable logic: Logic used in multiple components
  • Return useful values: State, functions, or objects
  • Keep them focused: One responsibility per hook
  • Document parameters: Clear what inputs are needed

❌ Don't:

  • Call conditionally: Hooks must be called in the same order
  • Call in loops: Violates Rules of Hooks
  • Call in regular functions: Only in components or other hooks
  • Make them too complex: Break into smaller hooks if needed

🔹 useDebounce Custom Hook

Delay updating a value until after a pause in changes. Useful for search inputs and API calls.

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;
}

// Using the Custom Hook
function SearchBar() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearch = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearch) {
      // API call happens only after user stops typing for 500ms
      console.log('Searching for:', debouncedSearch);
    }
  }, [debouncedSearch]);

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

🧠 Test Your Knowledge

What must custom hooks start with?