logo
eng-flag

Creating Custom Hooks in React

Introduction

Custom hooks are a powerful feature in React that allow you to extract component logic into reusable functions. They're a way to reuse stateful logic between components without adding more components to your tree. In this guide, we'll dive deep into creating custom hooks, their benefits, and best practices.

Table of Contents

  1. What are Custom Hooks?
  2. Why Use Custom Hooks?
  3. Rules of Hooks
  4. Creating Your First Custom Hook
  5. Advanced Custom Hook Patterns
  6. Best Practices
  7. Real-World Examples
  8. Conclusion

What are Custom Hooks?

Custom hooks are JavaScript functions that start with the prefix "use" and may call other hooks. They allow you to extract component logic into reusable functions, making your code more modular and easier to maintain.

Why Use Custom Hooks?

  1. Code Reusability: Extract common logic into a hook and use it across multiple components.
  2. Cleaner Components: Move complex logic out of your components, making them easier to read and understand.
  3. Easier Testing: Isolate logic in custom hooks for more straightforward unit testing.
  4. State Management: Manage local state in a more organized way.

Rules of Hooks

Before diving into creating custom hooks, it's crucial to understand the rules of hooks:

  1. Only call hooks at the top level of your function component or custom hook.
  2. Don't call hooks inside loops, conditions, or nested functions.
  3. Only call hooks from React function components or custom hooks.

Creating Your First Custom Hook

Let's create a simple custom hook that manages a counter:

import { useState } from 'react';

function useCounter(initialCount = 0) {
  const [count, setCount] = useState(initialCount);

  const increment = () => setCount(prevCount => prevCount + 1);
  const decrement = () => setCount(prevCount => prevCount - 1);
  const reset = () => setCount(initialCount);

  return { count, increment, decrement, reset };
}

export default useCounter;

Now you can use this hook in any component:

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

function CounterComponent() {
  const { count, increment, decrement, reset } = useCounter(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Advanced Custom Hook Patterns

1. Combining Multiple Hooks

You can combine multiple hooks to create more complex custom hooks:

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 {
        const response = await fetch(url);
        const json = await response.json();
        setData(json);
        setLoading(false);
      } catch (error) {
        setError(error);
        setLoading(false);
      }
    }

    fetchData();
  }, [url]);

  return { data, loading, error };
}

2. Hooks with Callbacks

Custom hooks can also accept callbacks as parameters:

import { useState, useCallback } from 'react';

function useToggle(initialState = false) {
  const [state, setState] = useState(initialState);

  const toggle = useCallback(() => {
    setState(prevState => !prevState);
  }, []);

  return [state, toggle];
}

3. Hooks with Cleanup

When your custom hook sets up subscriptions or timers, make sure to clean them up:

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

Best Practices

  1. Naming Convention: Always start your custom hook names with "use" to follow the React convention.

  2. Keep It Simple: Each hook should do one thing well. If a hook becomes too complex, consider breaking it into smaller hooks.

  3. Proper Dependencies: When using useEffect or useCallback in your custom hook, make sure to include all necessary dependencies in the dependency array.

  4. TypeScript Support: If you're using TypeScript, add proper typing to your custom hooks for better developer experience:

    import { useState } from 'react';
    
    interface UseCounterReturn {
      count: number;
      increment: () => void;
      decrement: () => void;
      reset: () => void;
    }
    
    function useCounter(initialCount: number = 0): UseCounterReturn {
      // ... implementation
    }
    
  5. Testing: Write unit tests for your custom hooks using the @testing-library/react-hooks package:

    import { renderHook, act } from '@testing-library/react-hooks';
    import useCounter from './useCounter';
    
    test('should increment counter', () => {
      const { result } = renderHook(() => useCounter());
    
      act(() => {
        result.current.increment();
      });
    
      expect(result.current.count).toBe(1);
    });
    

Real-World Examples

1. useLocalStorage

A custom hook to persist state in localStorage:

import { useState } 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.log(error);
      return initialValue;
    }
  });

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

  return [storedValue, setValue];
}

2. useMediaQuery

A hook to check if a media query matches:

import { useState, useEffect } from 'react';

function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const media = window.matchMedia(query);
    if (media.matches !== matches) {
      setMatches(media.matches);
    }
    const listener = () => setMatches(media.matches);
    media.addListener(listener);
    return () => media.removeListener(listener);
  }, [matches, query]);

  return matches;
}

3. useDebounce

A hook to debounce a rapidly changing value:

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

2024 © All rights reserved - buraxta.com