Custom Hooks

Create reusable logic with custom hooks in React

Understanding Custom Hooks

Custom Hooks are a way to extract component logic into reusable functions. They allow you to share stateful logic between components without changing your component hierarchy.

By creating custom hooks, you can encapsulate complex logic, make your components cleaner, and promote code reuse across your application.

Creating Custom Hooks

Custom hooks are JavaScript functions that start with "use" and may call other hooks. They follow the same rules as regular hooks and can use any of the built-in hooks.

Key Points:

  • Must start with "use"
  • Can use other hooks
  • Should be pure functions
  • Can return any value

Example: useWindowSize Hook

import { useState, useEffect } from 'react';

function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return windowSize;
}

// Using the custom hook
function ResponsiveComponent() {
  const { width, height } = useWindowSize();

  return (
    <div>
      Window size: {width} x {height}
    </div>
  );
}

Common Custom Hooks

1. useLocalStorage Hook

A hook for persisting state in localStorage with automatic synchronization.

import { useState, useEffect } from 'react';

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

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

  return [storedValue, setStoredValue];
}

// Usage
function UserPreferences() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div>
      <select value={theme} onChange={e => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
    </div>
  );
}

2. useFetch Hook

A hook for handling data fetching with loading and error states.

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        setError(err.message);
      } 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>;
  if (!data) return null;

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

3. useDebounce Hook

A hook for debouncing values, useful for search inputs and other frequent updates.

import { useState, useEffect } from 'react';

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

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

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

  return debouncedValue;
}

// Usage
function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearchTerm) {
      // Perform search
      console.log('Searching for:', debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);

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

Custom Hooks Best Practices

1. Single Responsibility

Each custom hook should do one thing and do it well. If a hook is doing too many things, consider breaking it down into smaller hooks.

2. Clear Naming

Name your hooks clearly and descriptively. The name should indicate what the hook does and what it returns.

3. Proper Cleanup

Always clean up subscriptions, event listeners, and other side effects in the useEffect cleanup function.

4. Documentation

Document your custom hooks with clear comments explaining their purpose, parameters, and return values.

Next Steps

Now that you understand custom hooks, you might want to explore: