Skip to main content

Overview

Radix UI components with internal state support both controlled and uncontrolled usage patterns, giving you flexibility in how you manage state.
This follows the same pattern as form elements in React: you can either let the component manage its own state (uncontrolled) or manage it yourself (controlled).

Controlled vs Uncontrolled

Uncontrolled Components

Uncontrolled components manage their own internal state. You provide an initial value, and the component handles updates. Example: Uncontrolled Accordion
The accordion manages which item is open internally. You don’t need to track state.

Controlled Components

Controlled components delegate state management to you. You provide the current value and an onChange handler. Example: Controlled Accordion
You manage the state, so you can read it, update it from anywhere, persist it, sync it with URL params, etc.

Prop Patterns

All stateful Radix components follow this pattern:
Use uncontrolled for simple cases. Use controlled when you need to read or manipulate state externally.

How It Works Internally

Radix uses the useControllableState hook to support both patterns. From packages/react/use-controllable-state/src/use-controllable-state.tsx:18:
This hook:
  1. Detects if a prop value is provided (controlled)
  2. Uses internal state if no prop (uncontrolled)
  3. Calls onChange in controlled mode
  4. Warns in development if you switch between modes

Component Examples

Dialog

Uncontrolled:
Controlled:
From packages/react/dialog/src/dialog.tsx:61:

Checkbox

Uncontrolled:
Controlled:

Accordion (Multiple Mode)

Uncontrolled:
Controlled:
From packages/react/accordion/src/accordion.tsx:161:

State Change Callbacks

All controlled components provide callbacks that fire when state changes.

Using Callbacks

Common Use Cases

1. Analytics tracking:
2. Persist to localStorage:
3. Sync with URL params:
4. Conditional logic:

Finite State Machines

Radix components use enumerated strings for state, not booleans.

Why Enums Over Booleans?

Boolean state:
Limitations:
  • Only two states
  • Doesn’t scale to complex states
  • Less explicit
Enumerated state:
Benefits:
  • Explicit states
  • Easy to extend (add ‘opening’, ‘closing’, etc.)
  • Self-documenting
  • Type-safe

State Attributes

Components expose state via data-state attributes:
From packages/react/accordion/src/accordion.tsx:372:
Helper function:

Extended States

Some components have richer state:

Advanced Patterns

Derived State

Compute values from component state:

Coordinated State

Manage multiple components together:

State Machines with XState

For complex state logic, integrate with state machine libraries:

Best Practices

1. Choose the Right Pattern

Use uncontrolled by default. Only use controlled when you need external access to state.
Use uncontrolled when:
  • Simple interactions
  • State doesn’t need to be read elsewhere
  • No persistence required
Use controlled when:
  • Need to read state
  • Sync with other state
  • Persist state
  • Conditional logic based on state
  • Analytics tracking

2. Don’t Switch Between Patterns

Never switch a component from controlled to uncontrolled (or vice versa) during its lifetime.
Avoid:
Radix will warn you in development if you do this.

3. Initialize State Consistently

Ensure default/initial values match:

4. Handle Edge Cases in Callbacks

Validate state changes:

Summary

Radix UI state management provides:
  • Flexibility - Choose controlled or uncontrolled
  • Consistency - Same pattern across all components
  • Callbacks - React to state changes
  • Explicit state - Enumerated strings, not booleans
  • Type safety - Full TypeScript support
The controlled/uncontrolled pattern gives you the flexibility to start simple and add complexity only when needed.