Agent Skill
2/7/2026

react-test

Dodds' React and testing patterns

O
objective
0GitHub Stars
1Views
npx skills add Objective-Arts/lens

SKILL.md

Namereact-test
DescriptionDodds' React and testing patterns

name: react-test description: "React and testing patterns"

Kent C. Dodds Patterns

Apply Kent C. Dodds' philosophy and patterns for React development and testing.

Core Philosophy

The Testing Trophy (not Pyramid)

        ╱╲
       ╱  ╲     E2E (few)
      ╱────╲
     ╱      ╲   Integration (most)
    ╱────────╲
   ╱          ╲ Unit (some)
  ╱────────────╲
 ╱              ╲ Static (ESLint, TypeScript)
╱────────────────╲

Key insight: Integration tests give the best confidence-to-effort ratio.

The Golden Rule

"Write tests. Not too many. Mostly integration."

Test Like Users

"The more your tests resemble the way your software is used, the more confidence they can give you."


Testing Patterns

1. Query Priority (in order of preference)

// BEST: Accessible to everyone
getByRole('button', { name: /submit/i })
getByLabelText('Email')
getByPlaceholderText('Enter email')
getByText('Welcome')

// GOOD: Semantic queries
getByAltText('Profile picture')
getByTitle('Close')

// OK: Test IDs (last resort)
getByTestId('submit-button')

Never use: container.querySelector, DOM structure queries

2. User Event Over fireEvent

// WRONG
fireEvent.click(button)
fireEvent.change(input, { target: { value: 'text' } })

// RIGHT
import userEvent from '@testing-library/user-event'

const user = userEvent.setup()
await user.click(button)
await user.type(input, 'text')

3. Avoid Implementation Details

// WRONG - tests implementation
expect(component.state.isOpen).toBe(true)
expect(wrapper.find('Modal').props().visible).toBe(true)

// RIGHT - tests behavior
expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByText('Modal content')).toBeVisible()

4. One Assertion Per Behavior (not per test)

// FINE - multiple assertions for one behavior
test('submitting the form shows success message', async () => {
  const user = userEvent.setup()
  render(<ContactForm />)

  await user.type(screen.getByLabelText(/email/i), 'test@example.com')
  await user.type(screen.getByLabelText(/message/i), 'Hello')
  await user.click(screen.getByRole('button', { name: /submit/i }))

  expect(screen.getByRole('alert')).toHaveTextContent(/success/i)
  expect(screen.queryByLabelText(/email/i)).not.toBeInTheDocument()
})

5. Avoid Cleanup That Hides Bugs

// WRONG - afterEach cleanup can hide issues
afterEach(() => {
  jest.clearAllMocks()
  cleanup()
})

// RIGHT - let Testing Library auto-cleanup
// It does this automatically between tests

6. Test Error States

test('shows error when submission fails', async () => {
  server.use(
    rest.post('/api/contact', (req, res, ctx) => {
      return res(ctx.status(500))
    })
  )

  const user = userEvent.setup()
  render(<ContactForm />)

  await user.click(screen.getByRole('button', { name: /submit/i }))

  expect(screen.getByRole('alert')).toHaveTextContent(/error/i)
})

React Component Patterns

1. Compound Components

// Instead of prop drilling
<Menu items={items} onSelect={onSelect} renderItem={renderItem} />

// Use compound components
<Menu>
  <Menu.Button>Options</Menu.Button>
  <Menu.List>
    <Menu.Item onSelect={() => {}}>Edit</Menu.Item>
    <Menu.Item onSelect={() => {}}>Delete</Menu.Item>
  </Menu.List>
</Menu>

2. Prop Collections and Getters

function useToggle() {
  const [on, setOn] = useState(false)
  const toggle = () => setOn(o => !o)

  // Prop getter - flexible
  const getTogglerProps = (props = {}) => ({
    'aria-pressed': on,
    onClick: () => {
      props.onClick?.()
      toggle()
    },
    ...props,
  })

  return { on, toggle, getTogglerProps }
}

// Usage
const { on, getTogglerProps } = useToggle()
<button {...getTogglerProps({ onClick: customHandler })}>
  {on ? 'ON' : 'OFF'}
</button>

3. State Reducer Pattern

function useToggle({ reducer = (state, action) => action.changes } = {}) {
  const [{ on }, dispatch] = useReducer(
    (state, action) => reducer(state, { ...action, changes: toggleReducer(state, action) }),
    { on: false }
  )

  const toggle = () => dispatch({ type: 'toggle' })
  return { on, toggle }
}

// Consumer can intercept state changes
const { on, toggle } = useToggle({
  reducer: (state, action) => {
    if (action.type === 'toggle' && clickedTooMany) {
      return state // Prevent change
    }
    return action.changes
  }
})

4. Control Props

function useToggle({ on: controlledOn, onChange } = {}) {
  const [internalOn, setInternalOn] = useState(false)

  // Is it controlled?
  const isControlled = controlledOn !== undefined
  const on = isControlled ? controlledOn : internalOn

  const toggle = () => {
    if (!isControlled) {
      setInternalOn(o => !o)
    }
    onChange?.(!on)
  }

  return { on, toggle }
}

Accessibility Patterns

1. Semantic HTML First

// WRONG
<div onClick={handleClick}>Click me</div>

// RIGHT
<button onClick={handleClick}>Click me</button>

2. ARIA When Needed

// Custom component needs ARIA
<div
  role="button"
  tabIndex={0}
  aria-pressed={isActive}
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      handleClick()
    }
  }}
>
  Toggle
</div>

3. Labels for Inputs

// WRONG
<input placeholder="Email" />

// RIGHT
<label>
  Email
  <input type="email" />
</label>

// OR
<label htmlFor="email">Email</label>
<input id="email" type="email" />

// OR (visually hidden label)
<label htmlFor="search" className="sr-only">Search</label>
<input id="search" placeholder="Search..." />

4. Focus Management

function Modal({ isOpen, onClose, children }) {
  const closeButtonRef = useRef()

  useEffect(() => {
    if (isOpen) {
      closeButtonRef.current?.focus()
    }
  }, [isOpen])

  // Trap focus inside modal
  // Return focus when closed
}

Code Review Checklist

When reviewing React code, check:

Testing

  • Tests use accessible queries (getByRole, getByLabelText)
  • Tests use userEvent, not fireEvent
  • Tests check behavior, not implementation
  • Error states are tested
  • No mocking of implementation details

Components

  • Semantic HTML used where possible
  • Form inputs have associated labels
  • Interactive elements are keyboard accessible
  • Custom hooks follow conventions (use* prefix)
  • Props have sensible defaults

Patterns

  • No unnecessary state (derived values computed)
  • Effects have correct dependencies
  • Cleanup functions in effects where needed
  • Error boundaries for async operations

Quick Reference

Instead ofUse
fireEvent.click()userEvent.click()
getByTestId()getByRole() or getByLabelText()
wrapper.find()screen.getByRole()
expect(state)expect(screen.getBy...)
<div onClick><button onClick>
Mock everythingMock at network boundary

Resources

Skills Info
Original Name:react-testAuthor:objective