Skip to content
All essays
WebMarch 10, 20249 min

Custom React Hooks Guide

Learn how to create reusable custom hooks to share logic across components

Ü
Ümit Uz
Mobile & Full Stack Developer

Custom hooks are the key to reusing stateful logic in React applications. They let you extract component logic into reusable functions, following the DRY principle and making your codebase more maintainable. Let's master the art of creating custom hooks.

What Are Custom Hooks?

Custom hooks are JavaScript functions that start with "use" and can call other hooks. They allow you to reuse stateful logic between components without changing your component hierarchy.

The Power of Abstraction

Custom hooks abstract complex logic behind simple interfaces. Components can use this logic without understanding the implementation details, similar to how built-in hooks work.

typescript
// Custom hook
function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });
  // Complex window resize logic
  return size;
}

// Component usage
function MyComponent() {
  const { width, height } = useWindowSize();
  return <div>Window: {width}x{height}</div>;
}

Naming Conventions

Always prefix custom hooks with "use". This is important for several reasons: it clearly indicates the hook uses other hooks, it enables ESLint's exhaustive-deps rule, and it follows React conventions.

typescript
// ✅ Good: Follows convention
function useLocalStorage(key, initialValue) { }

// ❌ Bad: Violates convention
function getLocalStorage(key, initialValue) { }

Building Your First Custom Hook

Let's create a simple hook for managing localStorage with state synchronization.

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

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue] as const;
}

This hook combines useState with localStorage, providing a seamless state management experience that persists across page reloads.

Hooks for Data Fetching

Data fetching is a perfect use case for custom hooks. Extract common patterns into reusable hooks.

typescript
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const abortController = new AbortController();

    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url, {
          signal: abortController.signal
        });
        if (!response.ok) throw new Error('Network error');
        const json = await response.json();
        setData(json);
        setError(null);
      } catch (err) {
        setError(err as Error);
      } finally {
        setLoading(false);
      }
    }

    fetchData();

    return () => abortController.abort();
  }, [url]);

  return { data, error, loading };
}

Hooks for Form Handling

Forms have repetitive logic for managing values, validation, and submission. Custom hooks can centralize this logic.

typescript
function useForm<T extends Record<string, any>>(
  initialValues: T,
  validate?: (values: T) => Record<keyof T, string>
) {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Record<keyof T, string>>({} as any);
  const [touched, setTouched] = useState<Record<keyof T, boolean>>({} as any);

  const handleChange = (name: keyof T) => (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    setValues(prev => ({ ...prev, [name]: e.target.value }));
  };

  const handleBlur = (name: keyof T) => () => {
    setTouched(prev => ({ ...prev, [name]: true }));
    if (validate) {
      const validationErrors = validate(values);
      setErrors(validationErrors);
    }
  };

  const handleSubmit = (callback: (values: T) => void) => (
    e: React.FormEvent
  ) => {
    e.preventDefault();
    if (validate) {
      const validationErrors = validate(values);
      setErrors(validationErrors);
      if (Object.keys(validationErrors).length === 0) {
        callback(values);
      }
    } else {
      callback(values);
    }
  };

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    handleSubmit
  };
}

Hooks for Browser APIs

Custom hooks excel at wrapping browser APIs with React's lifecycle.

typescript
function useGeolocation() {
  const [state, setState] = useState({
    loading: true,
    accuracy: null,
    altitude: null,
    altitudeAccuracy: null,
    heading: null,
    latitude: null,
    longitude: null,
    speed: null,
    error: null
  });

  useEffect(() => {
    const onSuccess = (position: GeolocationPosition) => {
      setState({
        loading: false,
        accuracy: position.coords.accuracy,
        altitude: position.coords.altitude,
        altitudeAccuracy: position.coords.altitudeAccuracy,
        heading: position.coords.heading,
        latitude: position.coords.latitude,
        longitude: position.coords.longitude,
        speed: position.coords.speed,
        error: null
      });
    };

    const onError = (error: GeolocationPositionError) => {
      setState(prev => ({
        ...prev,
        loading: false,
        error: error.message
      }));
    };

    navigator.geolocation.getCurrentPosition(onSuccess, onError);

    const watchId = navigator.geolocation.watchPosition(
      onSuccess,
      onError
    );

    return () => navigator.geolocation.clearWatch(watchId);
  }, []);

  return state;
}

Hooks with Event Listeners

Managing event listeners is another great use case for custom hooks.

typescript
function useEventListener<K extends keyof WindowEventMap>(
  eventName: K,
  handler: (event: WindowEventMap[K]) => void,
  element?: HTMLElement
) {
  const savedHandler = useRef(handler);

  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(() => {
    const targetElement = element || window;
    const isSupported = targetElement.addEventListener;

    if (!isSupported) return;

    const eventListener = (event: Event) =>
      savedHandler.current(event as WindowEventMap[K]);

    targetElement.addEventListener(eventName, eventListener);

    return () => {
      targetElement.removeEventListener(eventName, eventListener);
    };
  }, [eventName, element]);
}

Composable Hooks

Hooks are most powerful when they compose with each other. Build complex behavior by combining simple hooks.

typescript
function useOnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);

  useEventListener('online', () => setOnline(true));
  useEventListener('offline', () => setOnline(false));

  return online;
}

function useNetworkStatus() {
  const online = useOnlineStatus();
  const [connectionType, setConnectionType] = useState<ConnectionType | null>(null);

  useEffect(() => {
    if ('connection' in navigator) {
      const conn = (navigator as any).connection;
      setConnectionType(conn.effectiveType);

      const handleChange = () => setConnectionType(conn.effectiveType);
      conn.addEventListener('change', handleChange);

      return () => conn.removeEventListener('change', handleChange);
    }
  }, []);

  return { online, connectionType };
}

Hooks for Animation

Animation hooks abstract complex timing and state management.

typescript
function useCounter(duration: number) {
  const [count, setCount] = useState(0);

  useInterval(() => {
    setCount(c => c + 1);
  }, duration);

  return count;
}

function useInterval(callback: () => void, delay: number) {
  const savedCallback = useRef(callback);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    const tick = () => savedCallback.current();
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

Parameterized Hooks

Create flexible hooks by accepting configuration parameters.

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

  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

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

Testing Custom Hooks

Test hooks in isolation using @testing-library/react-hooks.

typescript
import { renderHook, act } from '@testing-library/react-hooks';

test('useCounter increments and decrements', () => {
  const { result } = renderHook(() => useCounter());

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Best Practices

  1. 1Keep hooks focused on a single responsibility
  2. 2Document hook parameters and return values
  3. 3Use TypeScript for type safety
  4. 4Test hooks in isolation
  5. 5Handle cleanup properly in useEffect
  6. 6Use useCallback for functions returned from hooks
  7. 7Compose simple hooks to build complex behavior
  8. 8Name hooks descriptively based on their purpose

Common Mistakes to Avoid

Don't call hooks inside loops or conditions. Don't call hooks from regular JavaScript functions. Don't forget to include all dependencies in useEffect. Don't create hooks that are too broad or do too much. Don't forget to handle cleanup and error cases.

Conclusion

Custom hooks are React's abstraction mechanism for reusing stateful logic. They let you extract complex logic from components into reusable, testable functions. By composing hooks together, you can build sophisticated features from simple building blocks.

Start by identifying repetitive logic in your components, extract it into a custom hook, and watch your codebase become more maintainable. Remember: the best hooks are small, focused, and composable.

Master custom hooks, and you'll write React code that's cleaner, more reusable, and easier to maintain. They're one of React's most powerful features for building scalable applications.

Related essays

Next essay
Web Performance Optimization Strategies