Build a Clean component logic
Principles: one responsibility per component, separate UI from logic (custom hooks), props as the public API (small + named), state colocated where used, derive don't store, lift state only when necessary, names that describe purpose not implementation. Avoid prop drilling > 2 levels (compose, context, or store). Test by behavior, not implementation.
Principles
1. One responsibility
A component does one thing. If it has a fetch + form + list + modal in one file, it's three components.
2. Separate UI from logic via custom hooks
function useChat(channelId) {
const { data } = useQuery(['messages', channelId], fetchMessages);
const send = useMutation(sendMessage);
return { messages: data, send: send.mutate };
}
function Channel({ channelId }) {
const { messages, send } = useChat(channelId);
return <ChatView messages={messages} onSend={send} />;
}Hook owns data + side effects. Component is dumb-ish, easy to test in Storybook.
3. Props are the public API
Small, named, typed:
type Props = { user: User; onLogout: () => void };
function Header({ user, onLogout }: Props) { ... }Avoid ...rest spread of unknown shape on stateful components.
4. State colocation
State lives at the deepest component that needs it. Don't lift state to the root just in case.
5. Derive, don't store
// BAD
const [count, setCount] = useState(0);
const [doubled, setDoubled] = useState(0);
useEffect(() => setDoubled(count * 2), [count]);
// GOOD
const [count, setCount] = useState(0);
const doubled = count * 2;useMemo only for expensive computations.
6. Compose over props for variants
Instead of <Card variant="summary" hasIcon hasFooter ...>, use composition:
<Card>
<Card.Header>...</Card.Header>
<Card.Body>...</Card.Body>
<Card.Footer>...</Card.Footer>
</Card>7. Naming
- Components: noun describing what (Button, MessageList).
- Hooks:
useThingdescribing what it provides (useUser, useChat). - Handlers:
onChange,onSubmit,onItemClick— verb afteron. - Boolean props:
is/has/can(isOpen, hasError, canEdit).
8. Avoid prop drilling
Two levels = fine. Five = smell. Options:
- Composition: pass children instead of props through middle layers.
- Context for cross-tree config (theme, auth).
- Store (Zustand) for cross-tree app state.
9. Effects are escape hatches
useEffect synchronizes with external systems (DOM APIs, subscriptions). Not for deriving state — derive in render. Not for fetching when you have React Query. Not for cascading state updates.
10. Test behavior, not implementation
Testing Library: simulate user actions, assert UI. Don't test internal state shape or hook order.
Anti-patterns
- God components (500+ lines, many responsibilities).
- Effects that derive state.
- Prop drilling > 2 levels.
- useMemo / useCallback everywhere reflexively.
- Boolean prop explosion.
- Magic strings as variants.
Code smell checklist
- File > 200 lines.
- > 5 useState calls — probably needs useReducer or split.
- > 3 useEffect calls — review whether they're necessary.
- Props with > 8 keys — split component.
- Repeated patterns — extract a hook.
Interview framing
"Each component does one thing. Custom hooks own data + effects; presentational components own JSX. Props are a small, named, typed public API. State colocated where used; derived values computed in render, not stored; effects only for external syncs (not for derivations or fetching when React Query handles it). Composition over boolean prop explosion. Avoid prop drilling more than two levels — compose, context, or store. Names describe intent (useChat, onMessageSend), not implementation (useStateManager, handleStuff). Test behavior with Testing Library, not internal hook order."
Follow-up questions
- •When is useEffect the wrong tool?
- •Show me how you'd split a god component.
- •When does composition beat props?
Common mistakes
- •Effects deriving state.
- •Boolean prop explosion.
- •Prop drilling.
- •useMemo everywhere.
Performance considerations
- •Splitting components helps memo work. Avoid prop instability from inline objects.
Edge cases
- •Forwarded refs for primitive wrappers.
- •Polymorphic components with as prop.
- •Headless component patterns.
Real-world examples
- •shadcn/ui, Radix primitives, Kent C. Dodds blog patterns.