Refactor React Hooks: Boost ERP Form Performance

by ADMIN 49 views

Overview

Hey team, let's talk about the ERP Stock Supply Web application. Right now, it's using two massive, all-in-one custom hooks. Think of them like giant, unwieldy tools. These hooks break the Single Responsibility Principle, making it tough to keep things running smoothly. They're hard to maintain, test, and reuse. That's why we're proposing a refactor to split these into smaller, focused, and testable units.

We're aiming for a composable architecture, following the best practices of React 19. We'll also draw inspiration from successful migrations by big players like Faire, Airbnb, and MUI X. This refactor is all about making our codebase cleaner, more efficient, and easier to work with. Let's dive in!


Problem Statement

Current Architecture Issues

1. Monolithic Hooks Violating Single Responsibility

Take a look at useStockReceiptForm.js. It's a beast, clocking in at 1,532 lines! Inside, there are 21 state variables, 40+ callbacks, and more than 10 modal orchestrations. It's trying to do way too much. The Single Responsibility Principle says each part of your code should do one thing and do it well. This hook is trying to juggle everything, making it a nightmare to test and debug.

Problems:

  • Testing Nightmare: Impossible to test in isolation because it relies on so many other things.
  • Debugging Hell: Navigating 1,500+ lines is a time sink.
  • Code Duplication: Modal patterns are repeated across hooks.
  • Performance Hit: Excessive re-renders slow things down.
  • No Reusability: Logic is locked up in the massive hook.

2. Duplicate Code Across Form Hooks

Both useStockReceiptForm and useStockSupplyForm are guilty of repeating code. We're seeing duplicate patterns for modal management, section collapse logic, status message handling, and more. This duplication adds up, leading to a lot of wasted effort.

Estimated Duplicate Code: ~500-600 lines across both hooks

3. Tight Coupling to Components

Components are tightly coupled to the hook structure, which makes refactoring tricky. When we update the hook, we risk breaking components that rely on it. This creates prop drilling, where data is passed through multiple levels of components. The components become overly dependent on the internal workings of the hook.


Proposed Solution

Phase 1: Extract Foundation Hooks (Weeks 1-2)

We'll create focused, reusable hooks that follow React 19 best practices. Let's break down the approach, shall we?

1.1. Extract Modal Management

File: src/hooks/useModalState.js

This hook simplifies modal management. It provides functions to open and close modals, reducing redundant code. It promotes reusability, and it's type-safe, thanks to TypeScript. It's a win-win!

import { useState, useCallback } from 'react'

export function useModalState(initialState = { open: false }) {
  const [state, setState] = useState(initialState)

  const open = useCallback((context = {}) => {
    setState({ open: true, ...context })
  }, [])

  const close = useCallback(() => {
    setState({ open: false })
  }, [])

  return { ...state, open, close }
}

Benefits:

  • 90% code reduction for modal state management
  • Reusable across all forms
  • Testable in isolation
  • Type-safe with TypeScript

1.2. Extract Section Management

File: src/hooks/useSections.js

This hook manages the state of collapsible sections. It provides functions to toggle, open all, and close all sections. This is another step towards cleaner, more maintainable code.

import { useState, useCallback } from 'react'

export function useSections(sections = []) {
  const [sectionOpen, setSectionOpen] = useState(() =>
    sections.reduce((acc, section) => ({ ...acc, [section]: true }), {})
  )

  const toggle = useCallback((key) => {
    setSectionOpen(prev => ({ ...prev, [key]: !prev[key] }))
  }, [])

  const openAll = useCallback(() => {
    setSectionOpen(Object.keys(sectionOpen).reduce((acc, key) => ({ ...acc, [key]: true }), {}))
  }, [sectionOpen])

  const closeAll = useCallback(() => {
    setSectionOpen(Object.keys(sectionOpen).reduce((acc, key) => ({ ...acc, [key]: false }), {}))
  }, [sectionOpen])

  return { sectionOpen, toggle, openAll, closeAll }
}

1.3. Extract Status Messages

File: src/hooks/useStatus.js

This hook manages status messages, like success or error notifications. It handles the display and automatic closing of these messages, making the user interface more user-friendly.

import { useState, useCallback, useEffect, useRef } from 'react'

export function useStatus(autoCloseDelay = 15000) {
  const [status, setStatus] = useState({ type: '', text: '' })
  const timerRef = useRef(null)

  const setSuccess = useCallback((text) => {
    setStatus({ type: 'success', text })
    if (autoCloseDelay) {
      timerRef.current = setTimeout(() => setStatus({ type: '', text: '' }), autoCloseDelay)
    }
  }, [autoCloseDelay])

  const setError = useCallback((text) => {
    setStatus({ type: 'error', text })
    if (autoCloseDelay) {
      timerRef.current = setTimeout(() => setStatus({ type: '', text: '' }), autoCloseDelay)
    }
  }, [autoCloseDelay])

  const dismiss = useCallback(() => {
    if (timerRef.current) clearTimeout(timerRef.current)
    setStatus({ type: '', text: '' })
  }, [])

  useEffect(() => {
    return () => {
      if (timerRef.current) clearTimeout(timerRef.current)
    }
  }, [])

  return { status, setSuccess, setError, dismiss }
}

1.4. Extract Metadata Loading

File: src/hooks/useMetadata.js

This hook handles the loading of metadata from the API. It manages loading states, error handling, and aborting requests. It ensures we're not making unnecessary API calls and keeps the UI updated.

import { useState, useEffect } from 'react'

export function useMetadata(fetchFn, params) {
  const [state, setState] = useState({
    meta: null,
    loading: true,
    error: null
  })

  useEffect(() => {
    const controller = new AbortController()
    let ignore = false

    async function loadMetadata() {
      try {
        setState(prev => ({ ...prev, loading: true, error: null }))

        const { success, meta, message } = await fetchFn({
          ...params,
          signal: controller.signal
        })

        if (ignore) return

        if (!success) {
          setState({ meta: null, loading: false, error: message || 'Failed to load metadata' })
          return
        }

        setState({ meta, loading: false, error: null })
      } catch (error) {
        if (error.name === 'AbortError' || ignore) return
        setState({ meta: null, loading: false, error: error.message })
      }
    }

    loadMetadata()

    return () => {
      controller.abort()
      ignore = true
    }
  }, [fetchFn, JSON.stringify(params)])

  return state
}

Phase 2: Extract Business Logic Hooks (Weeks 3-4)

Here we'll focus on the data and operations specific to our forms. Let's see how it's done.

2.1. Extract Form Data Management

File: src/components/stock/hooks/useStockReceiptData.js

This hook manages the form data, providing functions to update header and line items. It keeps the data organized, and it’s reusable.

import { useState, useCallback } from 'react'

export function useStockReceiptData(initialHeader = {}, initialLines = []) {
  const [header, setHeader] = useState(initialHeader)
  const [lines, setLines] = useState(initialLines)

  const updateHeader = useCallback((name, value) => {
    setHeader(prev => ({ ...prev, [name]: value }))
  }, [])

  const updateLine = useCallback((index, field, value) => {
    setLines(prev => {
      const copy = [...prev]
      copy[index] = { ...copy[index], [field]: value }
      return copy
    })
  }, [])

  const addLine = useCallback((line) => {
    setLines(prev => [...prev, line])
  }, [])

  const removeLine = useCallback((index) => {
    setLines(prev => prev.filter((_, i) => i !== index))
  }, [])

  const resetData = useCallback(() => {
    setHeader(initialHeader)
    setLines(initialLines)
  }, [initialHeader, initialLines])

  return {
    header,
    lines,
    updateHeader,
    updateLine,
    addLine,
    removeLine,
    resetData
  }
}

Phase 3: Compose Main Form Hook (Week 5)

Here's where it all comes together. We'll compose the main form hook using the smaller, reusable hooks.

File: src/components/stock/hooks/useStockReceiptForm.js (refactored)

This is where we bring everything together. By combining these hooks, we create a clear and organized form hook. This modular approach makes it easy to understand and maintain.

import { useMetadata } from '../../../hooks/useMetadata'
import { useSections } from '../../../hooks/useSections'
import { useStatus } from '../../../hooks/useStatus'
import { useStockReceiptData } from './useStockReceiptData'
import { useStockReceiptBrowse } from './useStockReceiptBrowse'
import { useStockReceiptCrud } from './useStockReceiptCrud'
import { fetchStockReceiptNewMeta } from '../../../services/api'

export default function useStockReceiptForm() {
  // Metadata loading
  const { meta, loading: metaLoading, error: metaError } = useMetadata(
    fetchStockReceiptNewMeta,
    {
      data: 'New',
      companyId: 1
    }
  )

  const { meta: detailMeta } = useMetadata(
    fetchStockReceiptNewMeta,
    {
      data: 'Detail',
      companyId: 1
    }
  )

  // Form data
  const formData = useStockReceiptData({}, [])

  // UI state
  const sections = useSections(['General Information', 'Delivery Information', 'Signature'])
  const statusMessage = useStatus(15000)

  // Browse modals
  const browseHandlers = useStockReceiptBrowse(
    formData.updateHeader,
    formData.updateLine,
    meta?.colMap
  )

  // CRUD operations
  const crud = useStockReceiptCrud(
    formData.header,
    formData.lines,
    meta,
    detailMeta
  )

  // Compose everything
  return {
    // Metadata
    meta,
    detailMeta,
    loading: metaLoading,
    error: metaError,

    // Form data
    ...formData,

    // UI state
    ...sections,
    ...statusMessage,

    // Browse modals
    ...browseHandlers,

    // CRUD
    ...crud
  }
}

Benefits:

  • Reduced from 1,532 lines to ~150 lines
  • Each sub-hook testable in isolation
  • Reusable across StockSupply and StockReceipt forms
  • Clear separation of concerns
  • Performance optimized (focused dependencies per hook)

Technical Approach

Architecture Diagram

Here’s a visual representation of how everything fits together. This diagram helps illustrate the relationship between the components and hooks.

graph TD
    A[StockReceiptForm Component] --> B[useStockReceiptForm]
    B --> C[useMetadata]
    B --> D[useStockReceiptData]
    B --> E[useSections]
    B --> F[useStatus]
    B --> G[useStockReceiptBrowse]
    B --> H[useStockReceiptCrud]

    G --> I[useModalState Γ— 10]
    H --> F

    C --> J[API: fetchStockReceiptNewMeta]
    D --> K[State: header, lines]
    H --> L[API: saveHeader, saveDetail, delete]

    style A fill:#e1f5ff
    style B fill:#ffe1e1
    style C fill:#e8f5e8
    style D fill:#e8f5e8
    style E fill:#e8f5e8
    style F fill:#e8f5e8
    style G fill:#fff3e1
    style H fill:#fff3e1
    style I fill:#f0f0f0

Acceptance Criteria

Functional Requirements

  • All existing functionality preserved (100% feature parity)
  • Both Stock Receipt and Stock Supply forms work identically to the current version
  • All 12 browse modals function correctly
  • Save, delete, edit, and new operations work without regression
  • Field mapping logic handles all backend inconsistencies
  • Print functionality unaffected

Non-Functional Requirements

  • Performance: No regression in render times (target: <100ms for form updates)
  • Bundle Size: No increase in production bundle size
  • Memory: Memory usage remains stable (no leaks from hooks)
  • Type Safety: All new hooks have PropTypes or TypeScript definitions
  • Accessibility: No ARIA or keyboard navigation regressions

Quality Gates

  • Test Coverage: 80%+ coverage on all extracted hooks
  • Code Quality: ESLint passes with no warnings
  • Documentation: All hooks have JSDoc comments with examples
  • Code Review: Approved by 2+ team members
  • Performance Testing: Lighthouse score β‰₯ 90 for form pages

Implementation Phases

We're breaking this down into manageable phases. Here’s the schedule for the refactor.

Phase 1: Foundation Hooks (Weeks 1-2) - 40 hours

  • Week 1: Create foundational hooks. This includes creating hooks for modal, section, and status management.
  • Week 2: Create hooks for data management and integrate those with the existing hooks.

Phase 2: Business Logic Hooks (Weeks 3-4) - 40 hours

  • Week 3: Create and test hooks for browse modals and CRUD operations.
  • Week 4: Continue creating hooks for business logic and validation.

Phase 3: Compose Main Hooks (Week 5) - 20 hours

  • Refactor the main hooks to use the extracted hooks and update existing components.
  • Perform end-to-end testing and performance optimization.

Phase 4: Testing & Optimization (Week 6) - 16 hours

  • Perform comprehensive integration testing, performance profiling, and memory leak testing.
  • Conduct an accessibility audit and cross-browser testing.
  • Finalize code reviews and documentation.

Success Metrics

Code Quality Metrics

Here are the metrics we'll use to measure the success of the refactor. We're aiming to significantly improve code quality.

Metric Before Target After Method
Lines per Hook 1,532 / 970 <200 Line count
Cyclomatic Complexity 45+ <10 per hook ESLint complexity plugin
Code Duplication ~40% <10% jscpd tool
Test Coverage 0% 80%+ Jest coverage report
Bundle Size 447 KB ≀450 KB Vite build analysis

Performance Metrics

We'll also keep a close eye on performance to ensure we're not slowing things down.

Metric Before Target After Tool
Initial Form Render 800ms <300ms React DevTools Profiler
Input Response Time 150-300ms <50ms Performance.now()
Modal Open Time 200ms <100ms React DevTools
Save Operation 2-3s <2s Network tab
Memory Usage 45MB + 2MB/100 rows No regression Chrome DevTools

Risk Analysis & Mitigation

Let's address potential risks and how we plan to manage them.

High Risk: Breaking Existing Functionality

  • Probability: Medium
  • Impact: High (production forms unusable)

Mitigation:

  1. Incremental approach: Refactor one hook at a time.
  2. Feature flags: Use environment variables to toggle the new and old implementations.
  3. Parallel testing: Run end-to-end tests against both the old and new implementations.
  4. Staged rollout: Deploy to dev, staging, then production with monitoring.

Medium Risk: Performance Regression

  • Probability: Low
  • Impact: Medium (slower forms frustrate users)

Mitigation:

  1. Performance benchmarks: Establish a baseline before refactoring.
  2. React DevTools Profiler: Monitor render times throughout the refactoring process.
  3. Memoization: Use React.memo, useMemo, and useCallback appropriately.
  4. Bundle analysis: Make sure there's no increase in JavaScript bundle size.

References & Research

Internal References

  • Current Implementation details are available in the code base.

External References


File Structure After Refactoring

This is how the file structure will look after the refactor:

src/
β”œβ”€β”€ hooks/                                    # Shared foundation hooks
β”‚   β”œβ”€β”€ useModalState.js                     # NEW: Generic modal state management
β”‚   β”œβ”€β”€ useSections.js                       # NEW: Collapsible section management
β”‚   β”œβ”€β”€ useStatus.js                         # NEW: Success/error status messages
β”‚   β”œβ”€β”€ useMetadata.js                       # NEW: API metadata loading
β”‚   β”œβ”€β”€ useDebounce.js                       # EXISTING: Already good pattern
β”‚   └── useDominantColor.js                  # EXISTING: Already good pattern
β”‚
β”œβ”€β”€ components/
β”‚   └── stock/
β”‚       β”œβ”€β”€ hooks/
β”‚       β”‚   β”œβ”€β”€ useStockReceiptForm.js       # REFACTORED: 1,532 β†’ 150 lines
β”‚       β”‚   β”œβ”€β”€ useStockReceiptData.js       # NEW: Form data management
β”‚       β”‚   β”œβ”€β”€ useStockReceiptBrowse.js     # NEW: Browse modal handlers
β”‚       β”‚   β”œβ”€β”€ useStockReceiptCrud.js       # NEW: Save/delete operations
β”‚       β”‚   β”œβ”€β”€ useStockReceiptValidation.js # NEW: Validation logic
β”‚       β”‚   β”‚
β”‚       β”‚   β”œβ”€β”€ useStockSupplyForm.js        # REFACTORED: 970 β†’ 120 lines
β”‚       β”‚   β”œβ”€β”€ useStockSupplyData.js        # NEW: Form data management
β”‚       β”‚   β”œβ”€β”€ useStockSupplyBrowse.js      # NEW: Browse modal handlers
β”‚       β”‚   β”œβ”€β”€ useStockSupplyCrud.js        # NEW: Save/delete operations
β”‚       β”‚   └── useStockSupplyValidation.js  # NEW: Validation logic
β”‚       β”‚
β”‚       └── __tests__/                       # NEW: Comprehensive test suite
β”‚           β”œβ”€β”€ useModalState.test.js
β”‚           β”œβ”€β”€ useSections.test.js
β”‚           └── integration/
β”‚               └── stockReceiptForm.integration.test.js
β”‚
└── docs/                                     # NEW: Documentation
    └── hooks/
        β”œβ”€β”€ architecture.md
        β”œβ”€β”€ foundation-hooks.md
        β”œβ”€β”€ business-hooks.md
        β”œβ”€β”€ testing.md
        └── migration.md

Lines of Code Impact:

  • Before: 2,502 lines (1,532 + 970)
  • After: ~1,200 lines total (including tests)
  • Reduction: ~52% fewer lines
  • Maintainability: 10x improvement (13 focused files vs 2 monoliths)

Labels: refactor, enhancement, architecture Milestone: Q1 2025 Estimated Effort: 6 weeks, 120 developer hours

That's the plan, folks! Let's work together to make our code cleaner, more efficient, and easier to work with. Any questions or thoughts? Let's discuss!