DEV Community

Hasan
Hasan

Posted on

Best Practices for Creating Reusable Custom Hooks in React

Custom hooks in React provide a powerful way to encapsulate and reuse logic across your application. They promote code reuse, enhance readability, and simplify state management. In this blog post, we'll explore best practices for creating reusable custom hooks in React using TypeScript, ensuring type safety and robustness.

Table of Contents

  1. Introduction
  2. Benefits of Custom Hooks
  3. Naming Conventions
  4. Keeping Hooks Simple
  5. Handling Side Effects
  6. Using Generics for Flexibility
  7. Providing Defaults and Options
  8. Testing Custom Hooks
  9. Documenting Your Hooks
  10. Conclusion

1. Introduction

Custom hooks are a key feature of React that allow developers to extract component logic into reusable functions. TypeScript further enhances custom hooks by providing type safety and preventing common errors. Let's delve into the best practices for creating reusable custom hooks in React with TypeScript.

2. Benefits of Custom Hooks

Before diving into best practices, let's review the benefits of using custom hooks:

  • Code Reusability: Custom hooks allow you to reuse logic across multiple components.
  • Readability: They help in breaking down complex logic into smaller, manageable functions.
  • Separation of Concerns: Custom hooks help in separating state management and side effects from the UI logic.

3. Naming Conventions

Naming your hooks properly is crucial for maintainability and readability. Always prefix your custom hook names with use to indicate that they follow the rules of hooks.

// Good
function useCounter() {
    // hook logic
}

// Bad
function counterHook() {
    // hook logic
}
Enter fullscreen mode Exit fullscreen mode

4. Keeping Hooks Simple

A custom hook should do one thing and do it well. If your hook becomes too complex, consider breaking it down into smaller hooks.

// Good
function useCounter(initialValue: number = 0) {
    const [count, setCount] = useState<number>(initialValue);

    const increment = () => setCount(count + 1);
    const decrement = () => setCount(count - 1);
    const reset = () => setCount(initialValue);

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

// Bad
function useComplexCounter(initialValue: number = 0, step: number = 1) {
    const [count, setCount] = useState<number>(initialValue);

    const increment = () => setCount(count + step);
    const decrement = () => setCount(count - step);
    const reset = () => setCount(initialValue);
    const double = () => setCount(count * 2);
    const halve = () => setCount(count / 2);

    return { count, increment, decrement, reset, double, halve };
}
Enter fullscreen mode Exit fullscreen mode

5. Handling Side Effects

When dealing with side effects, use the useEffect hook inside your custom hook. Ensure that side effects are properly cleaned up to prevent memory leaks.

import { useEffect, useState } from 'react';

function useFetchData<T>(url: string) {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState<boolean>(true);

    useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await fetch(url);
                const result = await response.json();
                setData(result);
            } catch (error) {
                console.error('Error fetching data:', error);
            } finally {
                setLoading(false);
            }
        };

        fetchData();
    }, [url]);

    return { data, loading };
}

export default useFetchData;
Enter fullscreen mode Exit fullscreen mode

6. Using Generics for Flexibility

Generics in TypeScript allow your hooks to be more flexible and reusable by supporting multiple types.

import { useState, useEffect } from 'react';

function useFetchData<T>(url: string): { data: T | null, loading: boolean } {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState<boolean>(true);

    useEffect(() => {
        const fetchData = async () => {
            const response = await fetch(url);
            const result = await response.json();
            setData(result);
            setLoading(false);
        };

        fetchData();
    }, [url]);

    return { data, loading };
}

export default useFetchData;
Enter fullscreen mode Exit fullscreen mode

7. Providing Defaults and Options

Providing sensible defaults and allowing options makes your hooks more versatile.

interface UseToggleOptions {
    initialValue?: boolean;
}

function useToggle(options?: UseToggleOptions) {
    const { initialValue = false } = options || {};
    const [value, setValue] = useState<boolean>(initialValue);

    const toggle = () => setValue(!value);

    return [value, toggle] as const;
}

export default useToggle;
Enter fullscreen mode Exit fullscreen mode

8. Testing Custom Hooks

Testing is crucial to ensure your custom hooks work correctly. Use React Testing Library and Jest to write tests for your hooks.

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

test('should use counter', () => {
    const { result } = renderHook(() => useCounter());

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

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

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

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

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

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

    expect(result.current.count).toBe(0);
});
Enter fullscreen mode Exit fullscreen mode

9. Documenting Your Hooks

Clear documentation helps other developers understand and use your hooks effectively. Include comments and usage examples.

/**
 * useCounter - A custom hook to manage a counter.
 *
 * @param {number} [initialValue=0] - The initial value of the counter.
 * @returns {object} An object containing the count value and functions to increment, decrement, and reset the count.
 *
 * @example
 * const { count, increment, decrement, reset } = useCounter(10);
 */
function useCounter(initialValue: number = 0) {
    const [count, setCount] = useState<number>(initialValue);

    const increment = () => setCount(count + 1);
    const decrement = () => setCount(count - 1);
    const reset = () => setCount(initialValue);

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

export default useCounter;
Enter fullscreen mode Exit fullscreen mode

10. Conclusion

Creating reusable custom hooks in React with TypeScript enhances code reusability, maintainability, and robustness. By following these best practices, you can ensure that your custom hooks are efficient, flexible, and easy to use.

Top comments (0)