Custom React Hooks: Building Reusable Logic & Patterns

Custom React Hooks: Building Reusable Logic & Patterns

Table of Contents

React JS is a powerful JavaScript library for building user interfaces with remarkable capabilities like components, props, and state management. Among React’s most significant advancements are Custom React Hooks, which enable developers to extract, compose, and reuse complex logic across applications. Custom Hooks transform how developers write React code by promoting reusability, maintainability, and clean architecture. This comprehensive guide explores custom hooks in depth, covering implementation, patterns, testing, and real-world applications.

Understanding the Custom Hooks Foundation: Custom hooks are advanced techniques built directly on React’s fundamental hook system. To understand the core hooks that power all custom hooks development—including useState, useEffect, and useContext—check out our comprehensive guide on React Hooks: The Complete Guide to Modern Web Development.

Understanding Custom React Hooks: Definition & Benefits

Custom React Hooks are JavaScript functions that leverage React’s built-in hooks (useState, useEffect, useContext, etc.) to create reusable, composable logic encapsulated outside components. They follow the naming convention “useSomething,” which signals to developers and tools that they follow hook rules.

The power of custom hooks lies in their composition capability:

  • Combine multiple hooks into a single, focused function
  • Extract component logic into reusable modules
  • Share solutions across your entire codebase
  • Maintain clean separation between UI and logic

Core Principle: “Extract, encapsulate, and share behavior.”

Key Benefits of Custom React Hooks

1. Code Reusability Custom hooks eliminate code duplication by encapsulating logic once and reusing it everywhere.

// Without custom hooks - logic duplicated across components
// Component A uses useState, useEffect, etc. to fetch data
// Component B duplicates this entire logic
// Component C duplicates it again

// With custom hooks - logic defined once
const useUserData = (userId) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchUserData(userId).then(setUser).catch(setError).finally(() => setLoading(false));
  }, [userId]);

  return { user, loading, error };
};

// Now all components use the same logic
function UserProfile({ userId }) {
  const { user, loading, error } = useUserData(userId);
  // Component code focused on UI only
}

2. Improved Readability Logic abstraction makes components cleaner and more focused on UI rendering.

3. Separation of Concerns Business logic lives in hooks; UI logic lives in components. This clear separation improves code organization and makes applications easier to navigate.

4. Testability Isolated hooks are easier to unit test independently from components.

5. Enhanced Performance Custom hooks can implement memoization, lazy evaluation, and optimization techniques that improve rendering efficiency.

6. Shareable Logic Custom hooks can be published as npm packages, contributing to the React ecosystem and helping other developers.

Building Custom React Hooks: Step-by-Step Guide

Creating custom hooks is straightforward. The key is understanding how to combine existing hooks to create new functionality.

Step 1: Identify Reusable Logic

Look for patterns you use repeatedly:

  • Form handling across multiple components
  • API data fetching patterns
  • Local storage synchronization
  • Authentication logic
  • Search and pagination

Step 2: Understand Hook Rules

Custom hooks must follow React’s hook rules:

  • Only call hooks at the top level (not in loops, conditions)
  • Only call hooks from React components or other hooks
  • Use the “use” prefix for hook naming

Step 3: Create the Hook Function

Extract logic into a new function:

// Simple custom hook for form inputs
function useForm(initialValues, onSubmit) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues(prev => ({ ...prev, [name]: value }));
  };

  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    try {
      await onSubmit(values);
    } catch (error) {
      setErrors({ submit: error.message });
    } finally {
      setIsSubmitting(false);
    }
  };

  return {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    setValues,
    setErrors
  };
}

Step 4: Use in Components

function LoginForm() {
  const form = useForm(
    { email: '', password: '' },
    async (values) => {
      const response = await loginAPI(values);
      return response;
    }
  );

  return (
    <form onSubmit={form.handleSubmit}>
      <input
        name="email"
        value={form.values.email}
        onChange={form.handleChange}
        onBlur={form.handleBlur}
      />
      {form.touched.email && form.errors.email && (
        <span>{form.errors.email}</span>
      )}
      
      <input
        name="password"
        type="password"
        value={form.values.password}
        onChange={form.handleChange}
        onBlur={form.handleBlur}
      />
      {form.touched.password && form.errors.password && (
        <span>{form.errors.password}</span>
      )}
      
      <button type="submit" disabled={form.isSubmitting}>
        {form.isSubmitting ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

Common Custom React Hooks Examples

useLocalStorage: Persistent Data Management

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: Persist user theme preference
function App() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

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

useApi: Data Fetching Hook

function useApi(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(url, options);
      
      if (!response.ok) {
        throw new Error(`API error: ${response.status}`);
      }
      
      const json = await response.json();
      setData(json);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [url, options]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  const refetch = useCallback(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch };
}

// Usage: Fetch user data
function UserProfile({ userId }) {
  const { data: user, loading, error, refetch } = useApi(`/api/users/${userId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button onClick={refetch}>Refresh</button>
    </div>
  );
}

usePagination: Managing Paginated Data

function usePagination(items, itemsPerPage = 10) {
  const [currentPage, setCurrentPage] = useState(1);

  const totalPages = Math.ceil(items.length / itemsPerPage);
  const startIndex = (currentPage - 1) * itemsPerPage;
  const endIndex = startIndex + itemsPerPage;
  const currentItems = items.slice(startIndex, endIndex);

  const goToPage = (pageNumber) => {
    const pageNum = Math.max(1, Math.min(pageNumber, totalPages));
    setCurrentPage(pageNum);
  };

  const nextPage = () => goToPage(currentPage + 1);
  const prevPage = () => goToPage(currentPage - 1);

  return {
    currentPage,
    totalPages,
    currentItems,
    goToPage,
    nextPage,
    prevPage,
    hasNextPage: currentPage < totalPages,
    hasPrevPage: currentPage > 1
  };
}

// Usage: Paginated product list
function ProductList({ products }) {
  const pagination = usePagination(products, 12);

  return (
    <div>
      <div className="products">
        {pagination.currentItems.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
      
      <div className="pagination">
        <button onClick={pagination.prevPage} disabled={!pagination.hasPrevPage}>
          Previous
        </button>
        <span>Page {pagination.currentPage} of {pagination.totalPages}</span>
        <button onClick={pagination.nextPage} disabled={!pagination.hasNextPage}>
          Next
        </button>
      </div>
    </div>
  );
}

useDebounce: Performance Optimization

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

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

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

  return debouncedValue;
}

// Usage: Search with debounced API calls
function SearchUsers() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 300);
  const { data: results } = useApi(`/api/search?q=${debouncedSearchTerm}`);

  return (
    <div>
      <input
        type="text"
        placeholder="Search users..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      
      <ul>
        {results?.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

useAsync: Complex Async Operations

function useAsync(asyncFunction, immediate = true) {
  const [status, setStatus] = useState('idle');
  const [value, setValue] = useState(null);
  const [error, setError] = useState(null);

  const execute = useCallback(async () => {
    setStatus('pending');
    setValue(null);
    setError(null);

    try {
      const response = await asyncFunction();
      setValue(response);
      setStatus('success');
      return response;
    } catch (error) {
      setError(error);
      setStatus('error');
      throw error;
    }
  }, [asyncFunction]);

  useEffect(() => {
    if (immediate) {
      execute();
    }
  }, [execute, immediate]);

  return { execute, status, value, error };
}

// Usage: Multi-step data loading
function DataProcessor() {
  const { execute: processData, status, value, error } = useAsync(
    async () => {
      const raw = await fetch('/api/data').then(r => r.json());
      const processed = await processLargeDataset(raw);
      return processed;
    },
    false // Don't execute immediately
  );

  return (
    <div>
      <button onClick={processData} disabled={status === 'pending'}>
        {status === 'pending' ? 'Processing...' : 'Process Data'}
      </button>
      
      {error && <p>Error: {error.message}</p>}
      {value && <p>Processed {value.count} items</p>}
    </div>
  );
}

Custom React Hooks Patterns and Best Practices

Pattern 1: Hook Composition

Combine multiple hooks to create more complex functionality:

function useAuthenticatedApi(url) {
  const { token, loading: authLoading } = useAuth();
  const { data, loading, error } = useApi(url, {
    headers: { Authorization: `Bearer ${token}` }
  });

  return { data, loading: authLoading || loading, error };
}

Pattern 2: Conditional Hooks with Wrapper

function useConditionalApi(url, shouldFetch) {
  const [data, setData] = useState(null);

  useEffect(() => {
    if (!shouldFetch) return;

    fetch(url)
      .then(r => r.json())
      .then(setData);
  }, [url, shouldFetch]);

  return data;
}

Pattern 3: Reducer-Based Hook

function useReducerHook(reducer, initialState) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return { state, dispatch };
}

Best Practices

  1. Use Descriptive Names: useUserData is better than useFetch
  2. Keep Hooks Focused: One responsibility per hook
  3. Document Return Values: Clear interfaces for hook consumers
  4. Handle Errors Gracefully: Include error states in returns
  5. Optimize Performance: Use useCallback and useMemo appropriately
  6. Follow Hook Rules: Call hooks at top level only

Custom Hooks vs Redux: When to Use Each

Choosing between custom hooks and Redux depends on your state management needs.

Detailed Comparison

AspectCustom HooksRedux
ComplexityLow to MediumHigh
Learning CurveEasySteep
BoilerplateMinimalSignificant
State ScopeComponent/LocalGlobal
DevToolsLimitedExcellent
ScalabilityGoodExcellent
Async OperationsManualMiddleware
Time Travel DebugNoYes
Best ForFeature-level stateGlobal state

Decision Framework

Use Custom Hooks When:

  • Managing component or feature-level state
  • State affects only few components
  • Simple to medium complexity
  • Don’t need time-travel debugging
  • Want minimal boilerplate

Use Redux When:

  • Managing global application state
  • Multiple unrelated components access state
  • Complex state interactions
  • Need Redux DevTools debugging
  • Large, complex application
  • Team familiar with Redux

Combination Approach: Use both! Custom hooks for local logic, Redux for global state:

// Custom hook for form logic
function useForm(initialValues, onSubmit) {
  // Hook implementation
}

// Redux for application state
// Redux stores global user, auth, theme, etc.

// Component combines both
function MyComponent() {
  const form = useForm({}, handleSubmit); // Local
  const user = useSelector(state => state.user); // Global
  
  return <form onSubmit={form.handleSubmit}>...</form>;
}

Testing Custom React Hooks

Testing custom hooks ensures reliability and prevents regressions.

React Testing Library

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

describe('useCounter', () => {
  it('increments count', () => {
    const { result } = renderHook(() => useCounter());

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

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

  it('decrements count', () => {
    const { result } = renderHook(() => useCounter(5));

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

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

  it('resets count', () => {
    const { result } = renderHook(() => useCounter(10));

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

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

Testing Hooks with Effects

it('fetches data on mount', async () => {
  const mockData = { id: 1, name: 'Test' };
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve(mockData)
    })
  );

  const { result, waitForNextUpdate } = renderHook(() => useApi('/api/test'));

  expect(result.current.loading).toBe(true);

  await waitForNextUpdate();

  expect(result.current.data).toEqual(mockData);
  expect(result.current.loading).toBe(false);
});

Publishing Custom Hooks to npm

Share your custom hooks with the React community:

1: Prepare Your Hook

// Ensure it's well-documented
/**
 * useApi - Fetch data from API
 * @param {string} url - API endpoint
 * @param {object} options - Fetch options
 * @returns {object} { data, loading, error, refetch }
 */
function useApi(url, options = {}) {
  // Implementation
}

export default useApi;

2: Create package.json

{
  "name": "use-api-hook",
  "version": "1.0.0",
  "description": "Custom React hook for API data fetching",
  "main": "index.js",
  "peerDependencies": {
    "react": "^16.8.0"
  },
  "keywords": ["react", "hooks", "api", "fetch"]
}

3: Publish to npm

npm publish

4: Document Usage

Include README with examples, parameters, and return values.

Real-World Use Cases for Custom React Hooks

Building a To-Do List Application

function useTodoList(initialTodos = []) {
  const [todos, setTodos] = useState(initialTodos);

  const addTodo = useCallback((text) => {
    setTodos(prev => [...prev, { id: Date.now(), text, completed: false }]);
  }, []);

  const removeTodo = useCallback((id) => {
    setTodos(prev => prev.filter(t => t.id !== id));
  }, []);

  const toggleTodo = useCallback((id) => {
    setTodos(prev => prev.map(t =>
      t.id === id ? { ...t, completed: !t.completed } : t
    ));
  }, []);

  return { todos, addTodo, removeTodo, toggleTodo };
}

function TodoApp() {
  const { todos, addTodo, removeTodo, toggleTodo } = useTodoList();
  const [input, setInput] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (input.trim()) {
      addTodo(input);
      setInput('');
    }
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Add a todo..."
        />
        <button type="submit">Add</button>
      </form>

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Implementing Dark Mode

function useDarkMode() {
  const [isDark, setIsDark] = useLocalStorage('darkMode', false);

  useEffect(() => {
    if (isDark) {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
  }, [isDark]);

  const toggle = useCallback(() => {
    setIsDark(prev => !prev);
  }, [setIsDark]);

  return { isDark, toggle };
}

function App() {
  const { isDark, toggle } = useDarkMode();

  return (
    <div className={isDark ? 'dark' : 'light'}>
      <button onClick={toggle}>
        {isDark ? 'Light Mode' : 'Dark Mode'}
      </button>
      {/* App content */}
    </div>
  );
}

Advantages of Custom React JS Hooks

Code Reusability: Write once, use everywhere across your application.

Improved Readability: Components become focused on UI, logic lives in hooks.

Separation of Concerns: Clear division between presentation and business logic.

Testability: Test hooks independently from components.

Enhanced Performance: Hooks enable memoization and optimization.

Shareable Logic: Publish and share with the React community.

Maintainability: Single source of truth for specific behaviors.

Frequently Asked Questions about Custom React Hooks

Can I use multiple custom hooks in a single component?

Yes, absolutely. Custom hooks are designed to be composable. You can use multiple hooks together to handle different aspects of your component’s functionality.

Is there any performance concern with custom hooks?

Custom hooks, when implemented correctly, improve performance through memoization and optimization. However, improper usage—like creating infinite loops or missing dependencies—can cause performance issues. Always follow hook rules.

Can I pass parameters to custom hooks?

Yes, custom hooks accept parameters like regular functions. This makes them flexible and customizable for different scenarios.

Are custom React Hooks a replacement for Redux?

Custom hooks handle component-level state, while Redux manages global state. They serve different purposes. For complex global state, Redux is more appropriate. For feature-level logic, custom hooks are ideal.

How can I share my custom hooks?

Publish them as npm packages with clear documentation. Include examples, parameter descriptions, and return value specifications. Popular packages like react-hook-form and react-query started as shared custom hooks.

What’s the difference between custom hooks and utility functions?

Custom hooks can call other hooks and use React features. Utility functions are regular functions without hook capabilities. Choose custom hooks when you need React’s hook system (useState, useEffect, etc.).

How do I debug custom hooks?

Use React DevTools to see hook values in real-time. Add console.log statements to track execution. Write unit tests to verify behavior. Use the useDebugValue hook to display debug information.

Continue Learning: Explore Related React Concepts

To deepen your understanding of custom hooks and related React patterns:

Conclusion: Mastering Custom React Hooks

Custom React Hooks are a transformative tool for modern React development. By enabling logic extraction, reuse, and composition, they help developers build cleaner, more maintainable applications. Whether you’re managing form state, fetching data, or implementing custom behavior, custom hooks provide elegant solutions.

The journey from understanding basic hooks to mastering custom hooks opens new possibilities for your React development. As you build more custom hooks, you’ll develop intuition about composability, hook design, and reusable patterns. The React ecosystem benefits tremendously when developers share their custom hooks, so consider publishing your creations to npm.

Custom hooks represent the maturity of React development practices. Embrace them, practice creating hooks, test thoroughly, and share your solutions with the community. By mastering custom hooks, you unlock the full potential of React and position yourself as a skilled, productive developer.

Start building your own custom hooks today, and watch your code quality, reusability, and maintainability soar to new heights!

SOURCEBAE: HIRE REACT DEVELOPERS

Picture of Priyanshu Pathak

Priyanshu Pathak

Priyanshu Pathak is a Senior Developer at Sourcebae. He works across the stack to build fast, reliable features that make hiring simple. From APIs and integrations to performance and security, Priyanshu keeps our products clean, scalable, and easy to maintain.

Table of Contents

Hire top 1% global talent now

Related blogs

Multimodal annotation is the practice of labeling two or more data types text, image, audio, video, or sensor streams within

Conversational AI annotation is the process of labeling user utterances with structured semantic layers intent classes, entity slots, dialogue acts,

Quick Answer React is primarily a front-end technology. It is a JavaScript library designed for building user interfaces that run

Quick Answer React is a library specifically, a JavaScript library for building user interfaces. This is not a matter of