All topics

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

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.

An extra render, plus a frame where the two arrays disagree.

Prefer

tsx
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

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.
}

There is always one render where the new user is displayed with the old user's input.

Prefer

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.
}

React discards the old instance entirely. There is no intermediate wrong state to leak.

Composition over configuration

Avoid

tsx
<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

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>

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.

Download
.claude/skills/react-component-patterns/SKILL.md

Drop 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