跪拜 Guibai
← All articles
React.js · TypeScript · Architecture

React's Real Refactor Isn't Components—It's Pulling Logic Into Custom Hooks

By 烬羽 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Teams that stop at component splitting still suffer tangled change surfaces—adding a feature means touching three or four files. Moving business logic into Hooks makes operations testable in isolation, keeps UI swaps cheap, and prevents the "300-line App.tsx" that every growing React project eventually hits.

Summary

Most React codebases split components but leave useState, CRUD, and filtering piled into a 300-line App.tsx. That only reorganizes JSX—it doesn't separate concerns. A custom Hook acts as a dedicated logic layer that owns all state and mutation functions, exposing data and callbacks to components that become pure render functions. App.tsx shrinks to a 20-line assembly point that wires Hook output to component props. The article walks through a Todo app refactor: TypeScript types define the data contract first, a useTodos Hook centralizes every operation with functional state updates, and components like TodoInput and TodoItem contain nothing but local UI state and event callbacks. Filtering logic stays in the Hook as derived state—computed, not stored—avoiding synchronization bugs. The piece also covers when to upgrade from multiple useState calls to useReducer, and gives a clear rule: only state shared across components belongs in a Hook; input-field state stays local.

Takeaways
Components split only JSX; business logic stays in App.tsx unless deliberately extracted into custom Hooks.
A custom Hook owns all state and mutation functions, exposing them to components that become pure render shells.
Functional state updates (prev => ...) prevent stale closures when multiple setState calls happen in the same event.
Derived state like filtered lists should be computed inside the Hook, never stored in a separate useState.
Input-field local state belongs in the component; only state shared by multiple components belongs in a Hook.
Upgrade from multiple useState calls to useReducer when a Hook has three or more independent setState operations and the logic becomes hard to track.
App.tsx becomes a thin wiring layer—20 lines instead of 300—that distributes Hook data and callbacks to components via props.
Conclusions

The distinction between splitting components and splitting logic explains why many React codebases feel clean to read but painful to change—the real coupling is in state and operations, not JSX.

Treating derived state as a computation rather than stored state is a principle that eliminates an entire class of synchronization bugs, yet it's frequently violated in practice.

The rule of thumb for useReducer—three or more independent setState calls—gives a concrete, testable threshold rather than a vague "when state logic gets complex."

Concepts & terms
Custom Hook
A JavaScript function whose name starts with 'use' that can call other React Hooks (useState, useEffect, etc.) internally. It encapsulates stateful logic without rendering JSX, acting as a reusable logic layer between components and React's state system.
Derived state
Data that can be fully computed from existing state or props, rather than stored independently. Computing it on each render avoids duplication and synchronization bugs—for example, a filtered list derived from a full list plus a filter criterion.
Functional update
Passing a function (prev => next) to a state setter instead of a direct value. React guarantees 'prev' is the latest state snapshot, preventing stale closures when multiple updates happen in the same synchronous event.
useReducer
A React Hook that manages complex state logic using a reducer function and dispatched actions. It centralizes multiple state transitions into a single switch statement, making state changes more predictable than scattered useState calls.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗