Best Practices for Creating Reusable Custom Hooks in React

RMAG news

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

Introduction
Benefits of Custom Hooks
Naming Conventions
Keeping Hooks Simple
Handling Side Effects
Using Generics for Flexibility
Providing Defaults and Options
Testing Custom Hooks
Documenting Your Hooks
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
}

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

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;

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;

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;

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

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;

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.