The goal of React component abstraction is not to reduce the number of files. It is to give a stable UI concept a clear API. Allow a little duplication first, then abstract when the common structure and variation points are real.
Function components are the default
New React code usually uses function components and Hooks:
type ButtonProps = {
children: React.ReactNode
variant?: "primary" | "secondary"
onClick?: () => void
}
export function Button({ children, variant = "primary", onClick }: ButtonProps) {
return (
<button className={`button button-${variant}`} onClick={onClick}>
{children}
</button>
)
}
Class components still exist in older projects and some Error Boundary patterns, but function components fit the current React API and ecosystem better.
When to abstract
Consider abstraction when at least two of these are true:
- The same recognizable UI concept appears in multiple places.
- Structure is stable, while content, state, or small styles vary.
- Interaction rules need one shared fix or test.
- The component has a clear domain name, not
CommonWrapper2. - Callers do not need to know internal DOM details.
Two copies of three lines of JSX do not always need a component. Repeated accessibility behavior, loading states, validation, or keyboard interactions should be centralized earlier.
Prefer composition over many boolean props
This API will become hard to reason about:
<Card compact bordered showHeader showFooter imageLeft loading />
Composition gives callers clearer regions:
function Card({ children }: { children: React.ReactNode }) {
return <article className="card">{children}</article>
}
Card.Header = function CardHeader({ children }: { children: React.ReactNode }) {
return <header className="card-header">{children}</header>
}
Card.Body = function CardBody({ children }: { children: React.ReactNode }) {
return <div className="card-body">{children}</div>
}
You can also use ordinary children, named props, or smaller components. Do not use a compound component pattern just to look advanced.
Where state should live
Place state at the closest common owner:
- Used by one component: local
useState. - Shared by siblings: lift to the nearest common parent.
- Shared deeply: Context.
- Complex transitions:
useReducer. - Reusable stateful logic: custom Hook.
Do not move every piece of state into a global store. The farther state is from where it is used, the harder updates are to understand.
For Hook choices, read React Hooks Guide: State, Effects, Context, and Reducers.
Controlled and uncontrolled components
A reusable input should be clear about who owns the value. A controlled component receives value and change handler:
type SearchInputProps = {
value: string
onChange: (value: string) => void
}
function SearchInput({ value, onChange }: SearchInputProps) {
return <input value={value} onChange={(event) => onChange(event.target.value)} />
}
Uncontrolled components keep value in the DOM or internally, which can be fine for simple forms or native API integration. Avoid switching a component between controlled and uncontrolled modes.
Common abstraction mistakes
- Creating many one-line "shared components" for one page.
- Adding a dozen boolean props to one component.
- Mixing data fetching, permissions, visual styling, and modal state in one file.
- Introducing global Context to avoid passing props two levels.
- Using
useEffectto synchronize values that could be derived from one source. - Reusing visuals while ignoring semantic HTML, focus, and keyboard behavior.
Server and Client Component boundary
In the Next.js App Router, display components can often stay on the server. Only the smallest interactive region needs "use client" for state, events, Effects, or browser APIs.
Do not turn a whole page into a Client Component because one button is interactive. The boundary is explained in Next.js Server vs Client Components.
Pre-abstraction checklist
- Is the concept truly repeated?
- Which parts are stable and which parts vary?
- Do prop names express domain meaning?
- Is the call site clearer than raw JSX?
- Does it preserve semantic HTML and accessibility?
- Would deleting the abstraction make the code simpler?
Good component APIs are small and composable. Bad abstractions collect special switches.
Related FreeMac guides
- For Hooks, read React Hooks Guide: State, Effects, Context, and Reducers.
- For overlay placement, read React Portal: Render Modals Outside Parent DOM.
- For Tailwind component boundaries, read Tailwind CSS 4 Guide: Setup, Theme, and Responsive Design.
Continue reading
React Hooks Guide: State, Effects, Context, and Reducers
Use React Hooks by responsibility: useState for local state, useEffect for external synchronization, useReducer for complex transitions, and useContext for shared values.
9 minReact Hydration Failed: Causes, Debugging, and Fixes
Debug server and client rendering mismatches caused by dates, random values, browser APIs, invalid HTML, local storage, locale differences, and DOM-changing extensions.
7 min readReact Portal: Render Modals Outside Parent DOM
Use React Portal for modals, dropdowns, tooltips, drawers, and toasts that must escape overflow, stacking context, or local layout boundaries.
Subscribe to FreeMac
Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.