6 min read
Component Patterns
Composition over configuration, hooks that obey the rules that matter, and effects you mostly shouldn't have written.
Principles
Most effects are a mistake
Syncing state to other state, transforming props into state, or computing derived values in an effect all cause an extra render and a window where the UI is wrong. Effects are for synchronizing with systems outside React — nothing else.
Compose with children instead of growing a prop list
A component with fourteen boolean props is a component that does fourteen things badly. Accept `children` or slots and let the caller assemble what they need.
A key change is a remount
When state must reset because the subject changed, `key={id}` is the entire solution. It replaces the effect-that-resets-state pattern, and it is correct in cases the effect version silently isn't.
Custom hooks own behaviour, components own markup
When a component's logic outgrows its JSX, the logic wants to be a hook. It becomes testable on its own and reusable in the next screen that needs the same behaviour.
Lift state only as far as it actually needs to go
State hoisted to a common ancestor 'just in case' re-renders that whole subtree forever. Push it down until something genuinely shared forces it up.
Patterns
What goes wrong, what to do instead, and why the difference matters.
Derived values
Avoid
const [items, setItems] = useState<Item[]>([]);
const [visible, setVisible] = useState<Item[]>([]);
useEffect(() => {
setVisible(items.filter((i) => !i.archived));
}, [items]);
// Renders once with stale `visible`, then again after the effect.An extra render, plus a frame where the two arrays disagree.
Prefer
const [items, setItems] = useState<Item[]>([]);
// Computed during render. Never stale, never an extra pass.
const visible = items.filter((item) => !item.archived);One render, always consistent. Add `useMemo` only if profiling says the filter is expensive.
Resetting state on prop change
Avoid
function ProfileForm({ userId }: Props) {
const [draft, setDraft] = useState("");
useEffect(() => {
setDraft(""); // clear when switching user
}, [userId]);
// Renders once with the *previous* user's draft still showing.
}There is always one render where the new user is displayed with the old user's input.
Prefer
// Parent — a new key remounts the form, resetting all its state.
<ProfileForm key={userId} userId={userId} />
function ProfileForm({ userId }: Props) {
const [draft, setDraft] = useState("");
// No effect, and no window where the state is wrong.
}React discards the old instance entirely. There is no intermediate wrong state to leak.
Composition over configuration
Avoid
<Card
title="Revenue"
subtitle="Q4"
showIcon
iconName="chart"
showFooter
footerText="Updated 2h ago"
footerAlign="right"
variant="elevated"
hasBorder={false}
/>Every new requirement adds a prop. The component accretes branches until nobody dares change it.
Prefer
<Card variant="elevated">
<Card.Header icon={<ChartIcon />}>
<Card.Title>Revenue</Card.Title>
<Card.Subtitle>Q4</Card.Subtitle>
</Card.Header>
<Card.Footer align="right">Updated 2h ago</Card.Footer>
</Card>New layouts need no changes to Card. The caller composes exactly what it needs.
Review checklist
What I look for when reviewing a pull request that touches this area.
- No effect exists purely to compute state from other state or props.
- State resets on identity change use `key`, not an effect.
- Components take `children` or slots rather than long boolean prop lists.
- Every effect has a genuine external system and a correct cleanup.
- State lives at the lowest level that works.
- Reusable behaviour is extracted into named custom hooks.
Take the skill
Everything above, generated into a skill file your agent can load. Copy it, or download it and commit it to your repo.
.claude/skills/react-component-patterns/SKILL.mdDrop into .claude/skills/ or .cursor/skills/ — the frontmatter drives when the skill loads.
---
name: react-component-patterns
description: >-
React component and hook design: composition over prop explosion, avoiding
unnecessary effects, key-based state reset, custom hook extraction, and
state colocation. Use when building or refactoring components.
argument-hint: "[component]"
license: MIT
metadata:
author: John Felix Lim
version: "1.0.0"
---
# Component Patterns
Apply when writing or reviewing components. Before adding an effect, ask what external system it synchronizes with — if the answer is 'none', it should be render-time logic or a key instead.
## Rules
1. **Most effects are a mistake** — Syncing state to other state, transforming props into state, or computing derived values in an effect all cause an extra render and a window where the UI is wrong. Effects are for synchronizing with systems outside React — nothing else.
2. **Compose with children instead of growing a prop list** — A component with fourteen boolean props is a component that does fourteen things badly. Accept `children` or slots and let the caller assemble what they need.
3. **A key change is a remount** — When state must reset because the subject changed, `key={id}` is the entire solution. It replaces the effect-that-resets-state pattern, and it is correct in cases the effect version silently isn't.
4. **Custom hooks own behaviour, components own markup** — When a component's logic outgrows its JSX, the logic wants to be a hook. It becomes testable on its own and reusable in the next screen that needs the same behaviour.
5. **Lift state only as far as it actually needs to go** — State hoisted to a common ancestor 'just in case' re-renders that whole subtree forever. Push it down until something genuinely shared forces it up.
## Patterns
### Derived values
**Avoid** — An extra render, plus a frame where the two arrays disagree.
```tsx
const [items, setItems] = useState<Item[]>([]);
const [visible, setVisible] = useState<Item[]>([]);
useEffect(() => {
setVisible(items.filter((i) => !i.archived));
}, [items]);
// Renders once with stale `visible`, then again after the effect.
```
**Prefer** — One render, always consistent. Add `useMemo` only if profiling says the filter is expensive.
```tsx
const [items, setItems] = useState<Item[]>([]);
// Computed during render. Never stale, never an extra pass.
const visible = items.filter((item) => !item.archived);
```
### Resetting state on prop change
**Avoid** — There is always one render where the new user is displayed with the old user's input.
```tsx
function ProfileForm({ userId }: Props) {
const [draft, setDraft] = useState("");
useEffect(() => {
setDraft(""); // clear when switching user
}, [userId]);
// Renders once with the *previous* user's draft still showing.
}
```
**Prefer** — React discards the old instance entirely. There is no intermediate wrong state to leak.
```tsx
// Parent — a new key remounts the form, resetting all its state.
<ProfileForm key={userId} userId={userId} />
function ProfileForm({ userId }: Props) {
const [draft, setDraft] = useState("");
// No effect, and no window where the state is wrong.
}
```
### Composition over configuration
**Avoid** — Every new requirement adds a prop. The component accretes branches until nobody dares change it.
```tsx
<Card
title="Revenue"
subtitle="Q4"
showIcon
iconName="chart"
showFooter
footerText="Updated 2h ago"
footerAlign="right"
variant="elevated"
hasBorder={false}
/>
```
**Prefer** — New layouts need no changes to Card. The caller composes exactly what it needs.
```tsx
<Card variant="elevated">
<Card.Header icon={<ChartIcon />}>
<Card.Title>Revenue</Card.Title>
<Card.Subtitle>Q4</Card.Subtitle>
</Card.Header>
<Card.Footer align="right">Updated 2h ago</Card.Footer>
</Card>
```
## Review checklist
- [ ] No effect exists purely to compute state from other state or props.
- [ ] State resets on identity change use `key`, not an effect.
- [ ] Components take `children` or slots rather than long boolean prop lists.
- [ ] Every effect has a genuine external system and a correct cleanup.
- [ ] State lives at the lowest level that works.
- [ ] Reusable behaviour is extracted into named custom hooks.
---
From the John Felix Lim Frontend Engineering Playbook — https://github.com/JohnFelixLim