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)
useDepositFormStatehandles form values, validation, and the Zod schemauseDepositCalculationshandles gas estimates, fee math, and min/max checksuseDepositTransactionwraps wagmiuseSendTransactionand tracks statususeGasManagerhandles EIP-1559 fee estimates, priority-fee logic, and buffer calculationuseSwapInforeads the LI.FI quote, route details, output amount, and slippageuseDepositOrchestratorcoordinates 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)
- A direct setter for hydration, testing, and optimistic updates
- An async fetch worker that calls
ApiServiceand updates its slice - 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.