The React ecosystem has a well-established culture of small components. "Split your components" is advice you will encounter in almost every guide, course, and code review. It is good advice applied to the right situations. Applied reflexively to every component over a certain size, it creates a different set of problems.

Photo by StartupStockPhotos on Pixabay
What Good Component Splitting Actually Achieves
Before getting into when to split, it helps to be clear about what splitting should achieve. Splitting a component is worthwhile when it results in at least one of the following:
Genuine reuse. The extracted component appears in multiple places in the application, or could plausibly do so. A Card component that appears on five different pages is a good candidate for extraction. A UserFormInternalHelper that only exists to break up a 300-line file is not.
Reduced responsibility. The extracted component has a narrower, more clearly defined purpose than the original. A component that handles both a form and the table it feeds is doing two things. Splitting it into a form component and a table component reduces the responsibility of each.
Improved testability. The extracted component can be tested in isolation without the full context of the parent. If the extracted component still requires mocking everything the parent does, the split may not have actually reduced complexity.
Clearer readability. The parent component's render logic becomes easier to follow because a complex section has been named and extracted. This is the weakest justification but still valid when the extracted piece represents a meaningful concept in the domain.
If a proposed split achieves none of these, it is probably not worth doing.
When Splitting Is the Wrong Answer
The most common over-engineering pattern is extracting components to reduce file length without addressing the actual structural problem. If a 400-line component file contains 400 lines of interleaved business logic, API calls, and UI rendering, splitting it into two 200-line files with the same mix of concerns has not improved anything. The structural problem is preserved at half the size.
Another common mistake is premature abstraction. A developer sees two components with similar JSX and creates a third component to unify them. Six months later the two use cases have diverged, the unified component has accumulated props to handle both cases, and removing the abstraction is more work than it would have been to keep the components separate.
The rule of three is a useful check: extract an abstraction only when you have three concrete use cases that it handles cleanly. Two use cases that look similar today may need to diverge; three cases with the same pattern are more likely to represent a genuine abstraction worth creating.
The Responsibility Test
The most reliable way to decide whether to split a component is to ask: can you describe what this component does in one sentence?
A component that "renders a user profile card with an edit button" has a clear, narrow responsibility. A component that "shows the current user's information, fetches their recent activity, handles updating their display name, and navigates to the settings page if they click the link" does not. The second description is four responsibilities. It probably needs to be split.
This test catches the case where a large component is large because it has genuinely too many concerns, not just because it has a lot of code. A component with one responsibility can be long. A component that handles authentication, data fetching, error display, and navigation is doing too much regardless of how many lines it is.
The Abstraction Gradient
React components exist on a spectrum from fully concrete (a specific screen or widget in your application) to fully generic (a button that appears in many places). Components at the generic end of the spectrum should have the most stable interfaces and the most rigorous design. Components at the concrete end can be more specific, more closely tied to the data they display.
The mistake to avoid is creating components in the middle of this spectrum that are neither specific enough to be directly useful nor generic enough to be reusable. These components end up with props that are too specific to generalize and too generic to document clearly. They accumulate conditional logic over time as their use cases multiply.
When you find yourself adding a variant prop to a component that already has type, mode, and size props, the component has grown beyond its intended abstraction level. At that point, splitting into two concrete components is usually cleaner than adding another layer of configuration.
React's official documentation covers component composition patterns and when to use children vs props for component flexibility.
Splitting vs. Custom Hooks
Not every large component should be split into smaller components. Sometimes the right split is between the component and a custom hook. If a component has complex state logic, side effects, or data transformation, extracting those into a hook often produces a cleaner result than extracting sub-components.
After extracting a custom hook, you may find that the component is already small enough to be clear. The hook handles all the non-UI logic; the component renders what the hook returns. No sub-components needed.
// Before: component mixes UI and logic
function UserDashboard() {
const [user, setUser] = useState(null);
const [stats, setStats] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
Promise.all([fetchUser(), fetchStats()])
.then(([user, stats]) => { setUser(user); setStats(stats); })
.finally(() => setIsLoading(false));
}, []);
if (isLoading) return <Spinner />;
return (
<div>
<UserProfile user={user} />
<StatsPanel stats={stats} />
</div>
);
}
// After: hook handles logic, component handles UI
function useUserDashboard() {
const [user, setUser] = useState(null);
const [stats, setStats] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
Promise.all([fetchUser(), fetchStats()])
.then(([user, stats]) => { setUser(user); setStats(stats); })
.finally(() => setIsLoading(false));
}, []);
return { user, stats, isLoading };
}
function UserDashboard() {
const { user, stats, isLoading } = useUserDashboard();
if (isLoading) return <Spinner />;
return (
<div>
<UserProfile user={user} />
<StatsPanel stats={stats} />
</div>
);
}
The dashboard component is now short not because the logic was split into sub-components, but because the logic was moved to where it belongs. Testing is the proof: the hook can be tested with a simple unit test, and the component can be tested with React Testing Library by passing mock data as props rather than mocking the entire data layer.

Photo by Brett Sayles on Pexels
A Practical Decision Process
When looking at a component and deciding whether to split it, run through these questions:
First, describe what the component does in one sentence. If you cannot, identify the distinct responsibilities and consider splitting along those lines.
Second, if you do split, will the extracted components be independently testable and potentially reusable? If neither, the split may not improve things.
Third, is the component large because of business logic that belongs in hooks or services? If so, extract the hooks first and see if the component becomes clear without any structural split.
Fourth, are there sub-components appearing more than two times in the same component that represent a meaningful domain concept? Those are worth extracting.
Splitting for file length alone, splitting before a pattern appears at least twice, and splitting in ways that produce tightly coupled pairs of components are the patterns most worth avoiding. ESLint with the react-hooks plugin helps catch when extracted hooks still have too many concerns, by flagging dependency arrays that have grown unwieldy.
For engineering teams working on React application structure, https://137foundry.com provides web development services including codebase reviews and refactoring guidance. The guide to structuring a React application for long-term maintainability covers folder organization, state placement, and TypeScript conventions in addition to component boundary decisions.
Top comments (0)