Architecture Patterns for Composable React Hooks in Next.js 15 with TypeScript (2026)

As React continues to evolve, especially looking ahead to Next.js 15 in 2026, the emphasis on composable patterns remains paramount. Functional components and Hooks have fundamentally reshaped how we manage state and side effects. This article delves into architectural strategies for crafting highly composable custom hooks using TypeScript, ensuring our applications are scalable, maintainable, and aligned with future best practices. We will focus on the foundational useState hook, demonstrating its power through practical, real-world examples.

1. Embracing useState for Composable State Management

The useState hook is the cornerstone of state management in React functional components. It allows us to declare state variables in a component and provides a function to update them. For composable architectures, the real power of useState shines when it's encapsulated within custom hooks, abstracting state logic away from UI concerns. Let's start with a simple, yet incredibly useful custom hook: useToggle.

useToggle abstracts the common pattern of managing a boolean state, such as for visibility, selection, or enablement. By wrapping useState, we create a reusable piece of logic that can be consumed across any component or even within other custom hooks.

REACT COMPONENT
// hooks/useToggle.ts
import { useState, useCallback } from 'react';

/**
 * @interface UseToggleResult
 * @description Defines the return type for the useToggle hook.
 * @property {boolean} isOpen - The current boolean state.
 * @property {() => void} toggle - Function to invert the current state.
 * @property {() => void} setTrue - Function to set the state to true.
 * @property {() => void} setFalse - Function to set the state to false.
 */
interface UseToggleResult {
  isOpen: boolean;
  toggle: () => void;
  setTrue: () => void;
  setFalse: () => void;
}

/**
 * @function useToggle
 * @description A custom hook for managing a boolean state with toggle, setTrue, and setFalse actions.
 * @param {boolean} [initialState=false] - The initial boolean state.
 * @returns {UseToggleResult} An object containing the current state and control functions.
 */
export function useToggle(initialState: boolean = false): UseToggleResult {
  const [isOpen, setIsOpen] = useState<boolean>(initialState);

  // Memoize functions to prevent unnecessary re-renders in consuming components
  const toggle = useCallback(() => setIsOpen(prev => !prev), []);
  const setTrue = useCallback(() => setIsOpen(true), []);
  const setFalse = useCallback(() => setIsOpen(false), []);

  return { isOpen, toggle, setTrue, setFalse };
}

In this example, useState(initialState) initializes our boolean state. The toggle, setTrue, and setFalse functions are wrapped in useCallback to ensure referential stability. This is a critical pattern for performance, preventing unnecessary re-renders of child components that depend on these functions, thereby enhancing composability without performance penalties.

2. Composing with useToggle: The useModal Hook

The true power of composable hooks emerges when they are combined to build more complex, domain-specific logic. Let's demonstrate this by building a useModal hook that leverages our useToggle hook. This pattern allows us to manage modal visibility without duplicating the basic boolean toggle logic, leading to cleaner, more readable, and highly maintainable code.

REACT COMPONENT
// hooks/useModal.ts
import { useToggle } from './useToggle'; // Assuming useToggle.ts is in the same directory

/**
 * @interface UseModalResult
 * @description Defines the return type for the useModal hook.
 * @property {boolean} isOpen - Indicates if the modal is currently open.
 * @property {() => void} openModal - Function to open the modal.
 * @property {() => void} closeModal - Function to close the modal.
 * @property {() => void} toggleModal - Function to toggle the modal's open state.
 */
interface UseModalResult {
  isOpen: boolean;
  openModal: () => void;
  closeModal: () => void;
  toggleModal: () => void;
}

/**
 * @function useModal
 * @description A custom hook for managing modal visibility, built on top of useToggle.
 * @param {boolean} [initialState=false] - The initial state of the modal (open or closed).
 * @returns {UseModalResult} An object containing the modal's state and control functions.
 */
export function useModal(initialState: boolean = false): UseModalResult {
  const { isOpen, toggle, setTrue, setFalse } = useToggle(initialState);

  return {
    isOpen,
    openModal: setTrue,  // Alias setTrue to openModal for better semantic clarity
    closeModal: setFalse, // Alias setFalse to closeModal
    toggleModal: toggle, // Alias toggle to toggleModal
  };
}

Here, useModal doesn't manage its own useState. Instead, it delegates the entire boolean state management to useToggle. This creates a clean separation of concerns: useToggle handles generic boolean state, while useModal provides a more semantically meaningful interface for modal-specific interactions. This layering is a fundamental principle of composable architectures.

3. Real-world Application: A Product Management Component

Now, let's see how these composable hooks integrate into a Next.js 15 application. We'll create a ProductList component that displays a list of products and allows adding new ones via a modal, all managed efficiently with our custom hooks.

REACT COMPONENT
// components/ProductList.tsx
import React from 'react';
import { useModal } from '../hooks/useModal'; // Adjust path as necessary

/**
 * @interface Product
 * @description Defines the structure for a product item.
 */
interface Product {
  id: string;
  name: string;
  price: number;
}

/**
 * @interface AddProductModalProps
 * @description Props for the AddProductModal component.
 */
interface AddProductModalProps {
  isOpen: boolean;
  onClose: () => void;
  onAddProduct: (productName: string, price: number) => void;
}

/**
 * @component AddProductModal
 * @description A simple modal component for adding new products.
 */
const AddProductModal: React.FC<AddProductModalProps> = ({ isOpen, onClose, onAddProduct }) => {
  const [productName, setProductName] = React.useState<string>('');
  const [productPrice, setProductPrice] = React.useState<number>(0);

  const handleSubmit = () => {
    if (productName.trim() && productPrice > 0) {
      onAddProduct(productName, productPrice);
      setProductName(''); // Clear form after submission
      setProductPrice(0);
      onClose();
    } else {
      alert('Please enter a valid product name and price.');
    }
  };

  if (!isOpen) return null; // Render nothing if the modal is not open

  return (
    <div style={{
      position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
      backgroundColor: 'rgba(0,0,0,0.5)', display: 'flex',
      justifyContent: 'center', alignItems: 'center', zIndex: 1000
    }}>
      <div style={{ background: 'white', padding: '25px', borderRadius: '8px', boxShadow: '0 4px 15px rgba(0,0,0,0.2)', minWidth: '300px' }}>
        <h3 style={{ marginTop: 0, marginBottom: '20px', textAlign: 'center' }}>Add New Product</h3>
        <input
          type="text"
          placeholder="Product Name"
          value={productName}
          onChange={(e) => setProductName(e.target.value)}
          style={{ display: 'block', marginBottom: '15px', padding: '10px', width: '100%', border: '1px solid #ccc', borderRadius: '4px' }}
        />
        <input
          type="number"
          placeholder="Price"
          value={productPrice}
          onChange={(e) => setProductPrice(parseFloat(e.target.value))}
          min="0"
          step="0.01"
          style={{ display: 'block', marginBottom: '20px', padding: '10px', width: '100%', border: '1px solid #ccc', borderRadius: '4px' }}
        />
        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
          <button onClick={onClose} style={{ padding: '10px 20px', backgroundColor: '#6c757d', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>Cancel</button>
          <button onClick={handleSubmit} style={{ padding: '10px 20px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>Add Product</button>
        </div>
      </div>
    </div>
  );
};

/**
 * @component ProductList
 * @description A functional component displaying a list of products and providing an interface to add new ones.
 */
const ProductList: React.FC = () => {
  // Use the composed useModal hook to manage modal state
  const { isOpen, openModal, closeModal } = useModal();
  
  // Local state for product list, managed by useState directly
  const [products, setProducts] = React.useState<Product[]>([
    { id: '1', name: 'Wireless Mouse', price: 29.99 },
    { id: '2', name: 'Mechanical Keyboard', price: 79.99 },
  ]);

  const handleAddProduct = React.useCallback((name: string, price: number) => {
    const newProduct: Product = {
      id: String(products.length + 1 + Math.random()), // More robust ID generation for demo
      name,
      price,
    };
    setProducts(prevProducts => [...prevProducts, newProduct]);
  }, [products.length]); // Recreate if products.length changes for ID uniqueness logic

  return (
    <div style={{ fontFamily: 'Inter, Arial, sans-serif', maxWidth: '800px', margin: '40px auto', padding: '30px', border: '1px solid #e0e0e0', borderRadius: '10px', boxShadow: '0 2px 10px rgba(0,0,0,0.05)' }}>
      <h1 style={{ textAlign: 'center', color: '#333', marginBottom: '30px' }}>Product Inventory</h1>
      
      <div style={{ marginBottom: '30px', textAlign: 'right' }}>
        <button onClick={openModal} style={{ padding: '12px 25px', backgroundColor: '#28a745', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '16px', fontWeight: 'bold' }}>
          + Add New Product
        </button>
      </div>

      {products.length === 0 ? (
        <p style={{ textAlign: 'center', color: '#666' }}>No products available. Click "Add New Product" to get started!</p>
      ) : (
        <ul style={{ listStyle: 'none', padding: 0, borderTop: '1px solid #eee' }}>
          {products.map(product => (
            <li key={product.id} style={{ borderBottom: '1px solid #eee', padding: '15px 0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <span style={{ fontSize: '1.1em', color: '#555' }}>{product.name}</span>
              <strong style={{ fontSize: '1.1em', color: '#333' }}>${product.price.toFixed(2)}</strong>
            </li>
          ))}
        </ul>
      )}

      {/* The modal component, controlled by useModal */}
      <AddProductModal
        isOpen={isOpen}
        onClose={closeModal}
        onAddProduct={handleAddProduct}
      />
    </div>
  );
};

export default ProductList;

The ProductList component effectively demonstrates the benefits of this architecture:

  • Clarity: The component's logic is focused on rendering and coordinating data, not on the intricate details of modal state management. const { isOpen, openModal, closeModal } = useModal(); is highly readable.
  • Reusability: useToggle and useModal can be effortlessly reused across different parts of the application, ensuring consistency and reducing boilerplate.
  • Maintainability: Changes to modal logic only affect useModal (or useToggle), not every component that uses a modal.
  • Testability: Each custom hook can be tested in isolation, simplifying the testing process for complex UIs.

As we advance towards Next.js 15 in 2026, embracing composable React Hooks with TypeScript will be increasingly vital for building robust and scalable front-end applications. By leveraging foundational hooks like useState to create specialized, reusable custom hooks, developers can achieve a cleaner separation of concerns, improve code readability, and significantly enhance the maintainability of their projects. This pattern fosters an ecosystem where complex UI behaviors are elegantly composed from smaller, well-defined, and testable units.

📚 More Resources

Check out related content:

Looking for beautiful UI layouts and CSS animations?

🎨 Need Design? Get Pure CSS Inspiration →
ℹ️ Note: Code is generated for educational purposes.

Comments

Popular posts from this blog

Optimizing Zustand State Architecture for Next.js 15 App Router & Server Components with TypeScript (2026)

How to Architect Resilient Authentication Systems in Next.js 15 with React & TypeScript (2026)

Effective TypeScript Patterns for Scalable Next.js 15 Logic Architectures (2026)