Building Better React Apps with Custom Hooks: From Basics to Advanced Patterns

Cover Image for Building Better React Apps with Custom Hooks: From Basics to Advanced Patterns
toofancoder
toofancoder

In earlier articles, we have covered the basics of React Custom Hooks and how to use them in our projects. If you haven't read them yet, you can find them Complete Guide to Custom Hooks with Examples.

In this article, we'll cover some more examples of custom hooks in ReactJS, best practices, factory pattern as well as testing a them and how to use them in our projects.

1. Understanding the Rules of Hooks

Before diving deeper into custom hooks, it's crucial to understand the fundamental rules that govern how they work in React:

Rule 1: Only Call Hooks at the Top Level

// ❌ Wrong - Hook inside condition
function Component() {
  if (someCondition) {
    const [state, setState] = useState(0);
  }
}

// ✅ Correct - Hook at top level
function Component() {
  const [state, setState] = useState(0);
  
  if (someCondition) {
    // Use state here
  }
}

Rule 2: Only Call Hooks from React Functions

// ❌ Wrong - Hook in regular function
function regularFunction() {
  const [state, setState] = useState(0);
}

// ✅ Correct - Hook in React component or custom hook
function useCustomHook() {
  const [state, setState] = useState(0);
  return state;
}

2. Building Progressive Custom Hooks

Let's build custom hooks step by step, starting with simple examples and progressing to more complex ones.

2.1 useToggle: Your First Custom Hook

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  
  const toggle = useCallback(() => {
    setValue(v => !v);
  }, []);

  return [value, toggle];
}

// Usage Example
function ExpandableSection() {
  const [isExpanded, toggleExpanded] = useToggle(false);
  
  return (
    <div>
      <button onClick={toggleExpanded}>
        {isExpanded ? 'Show Less' : 'Show More'}
      </button>
      {isExpanded && (
        <div>Extra content here...</div>
      )}
    </div>
  );
}

2.2 useDebounce: Managing Input Updates

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

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

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

  return debouncedValue;
}

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

  useEffect(() => {
    // Only search when debounced value changes
    if (debouncedSearchTerm) {
      performSearch(debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);

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

3. Advanced Custom Hook Example: useLocalStorage

The useLocalStorage hook is a custom hook that manages state stored in localStorage, allowing data persistence across page reloads.

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.error(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.error(error);
    }
  };

  return [storedValue, setValue];
}

// Usage of useLocalStorage in a component
function ThemeSwitcher() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

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

4. Advanced Hook Patterns

4.1 Composition of Hooks

Custom hooks can be composed together to create more powerful functionality:

function useAuthenticatedApi() {
  const { token } = useAuth(); // Custom auth hook
  const axiosInstance = useMemo(() => {
    return axios.create({
      headers: { Authorization: `Bearer ${token}` }
    });
  }, [token]);

  const makeRequest = useCallback(async (config) => {
    try {
      const response = await axiosInstance(config);
      return { data: response.data, error: null };
    } catch (error) {
      return { data: null, error };
    }
  }, [axiosInstance]);

  return { makeRequest };
}

4.2 Hook Factory Pattern

Sometimes you need to create variations of a hook based on configuration:

function createApiHook(baseURL) {
  return function useApi() {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(false);

    const fetchData = useCallback(async (endpoint) => {
      setLoading(true);
      try {
        const response = await fetch(`${baseURL}${endpoint}`);
        const json = await response.json();
        setData(json);
      } finally {
        setLoading(false);
      }
    }, []);

    return { data, loading, fetchData };
  };
}

// Create specific API hooks
const useUsersApi = createApiHook('https://api.example.com/users');
const useProductsApi = createApiHook('https://api.example.com/products');

5. Testing Custom Hooks

Testing custom hooks requires special consideration. Here's how to effectively test them:

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

// Test for useToggle hook
describe('useToggle', () => {
  it('should toggle value', () => {
    const { result } = renderHook(() => useToggle(false));

    expect(result.current[0]).toBe(false);

    act(() => {
      result.current[1]();
    });

    expect(result.current[0]).toBe(true);
  });
});

6. Performance Optimization Tips

When working with custom hooks, consider these optimization strategies:

  1. Memoization of Values:
function useCalculation(value) {
  const expensiveValue = useMemo(() => {
    return performExpensiveCalculation(value);
  }, [value]);

  return expensiveValue;
}
  1. Callback Memoization:
function useEventHandler() {
  const [data, setData] = useState(null);

  const handleEvent = useCallback((event) => {
    setData(event.target.value);
  }, []); // Empty deps array since it doesn't depend on any values

  return { data, handleEvent };
}
  1. Avoiding Unnecessary Re-renders:
function useDataFetching() {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null
  });

  // Better than having separate useState for each property
  const updateState = useCallback((updates) => {
    setState(prev => ({ ...prev, ...updates }));
  }, []);

  return [state, updateState];
}

7. Real-World Applications

Let's look at some practical scenarios where custom hooks shine:

  1. Form Management
  2. Authentication
  3. API Integration
  4. WebSocket Connections
  5. Browser APIs (localStorage, mediaQueries, etc.)
  6. Animation Controls
  7. State Management
  8. Device Features (camera, geolocation, etc.)

Each of these areas benefits from the encapsulation and reusability that custom hooks provide.

8. Future of Custom Hooks

As React continues to evolve, custom hooks are becoming increasingly important in modern React development. They're not just a pattern but a fundamental building block for sharing logic in React applications. Keep an eye on:

  • Integration with React Server Components
  • New patterns emerging from the community
  • Performance optimizations in React 19
  • Integration with other React features like Suspense

Remember, the best custom hooks are those that solve specific problems while remaining flexible enough to be used in various situations. Start simple, and add complexity only when needed.