Building Reusable React Components: The Right Way
Learn how to design components with single responsibility, proper typing, and clear interfaces — and why separating UI from business logic matters.

One of the most common mistakes in React development is mixing business logic with UI rendering inside the same component. This might seem harmless at first — the component works, after all — but it creates a tightly coupled, untestable, and unreusable piece of code. ## The Single Responsibility Principle Every component should have one job. A Button renders a button and handles click events. A UserCard renders user information. A UserTable renders a list of users. None of them should be making API calls directly. **Bad:** ```tsx export function Button() { const submitUser = async () => { await fetch("/api/users"); }; return <button onClick={submitUser}>Submit</button>; } ``` This Button is now permanently tied to the user API. You can't reuse it for anything else. **Good:** ```tsx type ButtonProps = { children: React.ReactNode; onClick?: () => void; disabled?: boolean; type?: "button" | "submit" | "reset"; }; export function Button({ children, onClick, disabled = false, type = "button" }: ButtonProps) { return ( <button type={type} onClick={onClick} disabled={disabled}> {children} </button> ); } ``` This Button works anywhere in your application. ## Size Guidelines Components should stay manageable: - **Small**: 20–80 lines — simple, pure UI - **Medium**: 80–150 lines — composed UI with some state - **Large**: up to ~200 lines — complex sections If a component exceeds 200 lines, it's a signal to break it apart. A UserPage that renders a filter, table, and modal should be decomposed into UserFilter, UserTable, and UserFormModal — each in its own file. ## Props Design Props are the public API of your component. Design them intentionally: - Use typed interfaces, never raw `any` - Name boolean props naturally: `isLoading`, `isDisabled`, `hasError` — not `loading`, `disabled`, or `flag` - If props exceed ~6 fields, consider passing a single object instead of individual values ```tsx // Hard to read <UserForm id={id} name={name} email={email} phone={phone} role={role} status={status} /> // Better <UserForm user={user} /> ``` ## Where Components Live The project structure should reflect component responsibility: - `components/ui/` — Base UI components (Button, Input, Badge). No business logic. - `components/layout/` — Layout shells (Navbar, Sidebar, Footer). - `components/shared/` — Reusable cross-feature components (StatusBadge, ConfirmDialog). - `features/feature-name/components/` — Feature-specific components that aren't shared. This separation makes it immediately clear what a component does, who owns it, and whether it can be reused.

