Field Notes

React Hooks

From 1,500-Line Monolith Hooks to 21 Testable Modules

I inherited a 75K-line crypto casino where three transaction hooks had grown to roughly 1,500 lines each, mixing form state, gas estimation, API calls, and execution. I split them into 21 focused hooks across 4,845 lines, so failures could be investigated in the relevant module.

Topic
React Hooks
Year
Read
6 min

The inheritance

The pain

When a deposit failed during gas estimation, the path ran through about 1,500 lines of connected logic. Form state affected gas calculations, which affected API calls and transaction execution. A small bug could take hours to isolate.

The refactor

I split each monolith into one orchestrator and a set of focused child hooks:

DEPOSIT (6 hooks)

  • useDepositFormState handles form values, validation, and the Zod schema
  • useDepositCalculations handles gas estimates, fee math, and min/max checks
  • useDepositTransaction wraps wagmi useSendTransaction and tracks status
  • useGasManager handles EIP-1559 fee estimates, priority-fee logic, and buffer calculation
  • useSwapInfo reads the LI.FI quote, route details, output amount, and slippage
  • useDepositOrchestrator coordinates those hooks and handles success and error flows

WITHDRAWAL (5 hooks)

The withdrawal flow has separate hooks for the form, calculations, transaction, network check, and orchestration.

SWAP (7 hooks)

The swap flow separates the form, quote, allowance, gas manager, token prices, transaction, and orchestration work.

TIP (3 hooks)

The tip flow keeps form state, transaction work, and orchestration separate.

Total

The result is 21 hooks across 4,845 lines. useGasManager has no form knowledge, and useDepositFormState has no gas knowledge. The orchestrator is responsible for joining the pieces.

The three-step store pattern (enforced across all 32 Zustand slices)

  1. A direct setter for hydration, testing, and optimistic updates
  2. An async fetch worker that calls ApiService and updates its slice
  3. An entry-point guard that checks cache age and in-flight deduplication before calling the worker
  • The token slice adds five retry attempts when a fetch fails

Service layer

Six class-based singletons, ApiService, WebSocketService, TransactionService, LocalStorageService, AuthService, and ThemeService, use an explicit getInstance() and destroy() lifecycle. Avoiding module-level instances keeps initialization order predictable, testable, and visible in DevTools.

Result

Continue conversation

Frontend problem worth thinking through?

Bring the context, including the hard parts. I am happy to talk them through.