Reactive is a proxy object that provides reactive versions of HTML elements and React components, allowing you to pass observables as props using the $ prefix.
Usage
import { Reactive } from '@legendapp/state/react'
import { state$ } from './state'
function Component() {
return (
<div>
{/* Use $ prefix for reactive props */}
<Reactive.input $value={state$.inputValue} />
<Reactive.div $className={state$.theme}>
<Reactive.span $style={state$.textStyle}>
{state$.message.get()}
</Reactive.span>
</Reactive.div>
</div>
)
}
Signature
const Reactive: IReactive
interface IReactive {
[elementName: string]: FC<ShapeWith$<ElementProps>>
}
How it works
Reactive is a proxy that:
- Intercepts property access (e.g.,
Reactive.div)
- Creates a reactive wrapper around that element/component
- Automatically handles props prefixed with
$ as observables
- Passes through regular props as-is
Props
Any component accessed through Reactive accepts:
Observable version of any standard prop. The $ prefix indicates the prop should reactively update when the observable changes.Examples:
$value for input value
$className for CSS classes
$style for style objects
$disabled for disabled state
$children for child content
Standard React props work normally without the $ prefix.
Examples
<Reactive.input
$value={state$.name}
placeholder="Enter your name"
/>
Reactive styling
<Reactive.div
$className={state$.theme}
$style={() => ({
backgroundColor: state$.bgColor.get(),
color: state$.textColor.get()
})}
>
Content
</Reactive.div>
Reactive attributes
<Reactive.button
$disabled={state$.isSubmitting}
onClick={handleClick}
>
Submit
</Reactive.button>
Reactive children
<Reactive.div $children={state$.message} />
// Equivalent to:
<Reactive.div>{state$.message.get()}</Reactive.div>
Complex reactive props
<Reactive.div
$className={() => {
const isActive = state$.isActive.get()
const theme = state$.theme.get()
return `container ${theme} ${isActive ? 'active' : ''}`
}}
>
Content
</Reactive.div>
function Form() {
return (
<form>
<Reactive.input
type="text"
$value={state$.form.name}
placeholder="Name"
/>
<Reactive.input
type="email"
$value={state$.form.email}
placeholder="Email"
/>
<Reactive.textarea
$value={state$.form.message}
placeholder="Message"
/>
<Reactive.select $value={state$.form.category}>
<option value="general">General</option>
<option value="support">Support</option>
<option value="sales">Sales</option>
</Reactive.select>
<Reactive.input
type="checkbox"
$checked={state$.form.subscribe}
/>
</form>
)
}
Conditional attributes
<Reactive.button
$disabled={() => !state$.isValid.get() || state$.isSubmitting.get()}
$className={() => state$.isSubmitting.get() ? 'loading' : ''}
onClick={handleSubmit}
>
Submit
</Reactive.button>
Reactive lists
<Reactive.ul $children={() => {
const items = state$.items.get()
return items.map(item => <li key={item.id}>{item.name}</li>)
}} />
With custom components
import { Button } from './Button'
// Make your component reactive
const ReactiveButton = reactive(Button, ['disabled', 'loading'])
function App() {
return (
<ReactiveButton
$disabled={state$.isSubmitting}
$loading={state$.isLoading}
onClick={handleClick}
>
Click me
</ReactiveButton>
)
}
SVG elements
<svg width="100" height="100">
<Reactive.circle
cx="50"
cy="50"
$r={state$.radius}
$fill={state$.color}
/>
</svg>
Animation
<Reactive.div
$style={() => ({
transform: `translateX(${state$.position.x.get()}px) translateY(${state$.position.y.get()}px)`,
opacity: state$.opacity.get()
})}
>
Animated content
</Reactive.div>
Behavior
Automatic subscription
Reactive props automatically subscribe to observable changes:
// This re-renders only the className when theme changes
<Reactive.div $className={state$.theme}>
Static content
</Reactive.div>
For form elements, reactive props are automatically bound:
<Reactive.input $value={state$.text} />
// Equivalent to:
<input
value={state$.text.get()}
onChange={(e) => state$.text.set(e.target.value)}
/>
Mixing reactive and static props
<Reactive.div
className="container" // Static
$style={state$.style} // Reactive
id="main" // Static
$title={state$.title} // Reactive
>
Content
</Reactive.div>
Function props
Reactive props can be functions that compute values:
<Reactive.div
$className={() => {
// This tracks both observables
const theme = state$.theme.get()
const size = state$.size.get()
return `${theme} ${size}`
}}
/>
ForwardRef support
Reactive components support refs:
function Component() {
const inputRef = useRef<HTMLInputElement>(null)
return (
<Reactive.input
ref={inputRef}
$value={state$.text}
/>
)
}
Configuration
Enable reactive elements
Reactive HTML elements are configured through enableReactive:
import { enableReactive } from '@legendapp/state/react-reactive/enableReactive'
import { configureReactive } from '@legendapp/state/react'
enableReactive(configureReactive)
This is called automatically in non-test environments.
Use configureReactive to add custom reactive components:
import { configureReactive } from '@legendapp/state/react'
import { MyComponent } from './MyComponent'
configureReactive({
MyComponent: {
component: MyComponent,
binders: {
value: {
handler: 'onChange',
getValue: (e) => e.target.value
}
}
}
})
Fine-grained updates
Reactive props only update the specific prop that changed:
// Only className updates when theme changes
// Only style updates when bgColor changes
<Reactive.div
$className={state$.theme}
$style={() => ({ backgroundColor: state$.bgColor.get() })}
/>
Avoid unnecessary computations
// ❌ Recomputes entire style object on any state change
<Reactive.div $style={state$.styleConfig} />
// ✅ Only computes when specific values change
<Reactive.div $style={() => ({
color: state$.textColor.get(),
fontSize: state$.fontSize.get()
})} />
Comparison with regular components
Without Reactive
const Component = observer(() => {
return (
<div className={state$.theme.get()}>
<input
value={state$.text.get()}
onChange={(e) => state$.text.set(e.target.value)}
/>
</div>
)
})
With Reactive
function Component() {
return (
<Reactive.div $className={state$.theme}>
<Reactive.input $value={state$.text} />
</Reactive.div>
)
}
Type safety
Reactive components are fully type-safe:
// ✅ TypeScript knows $value should be Observable<string>
<Reactive.input $value={state$.text} />
// ❌ TypeScript error - wrong type
<Reactive.input $value={state$.numberValue} />
// ✅ Regular props work normally
<Reactive.input type="email" placeholder="Email" />
When to use Reactive
Use Reactive when:
- You want fine-grained reactivity for specific props
- Building forms with two-way data binding
- Animating styles or attributes
- You want minimal component re-renders
- You prefer a more declarative syntax
Use observer instead when:
- Component logic is complex
- You need access to multiple observables throughout the component
- You want automatic tracking of all observable access
- Component re-renders are not a performance concern
Notes
- Not available in test environments by default (use
enableReactive manually)
- Props with
$ prefix must be observables or functions returning values
- Two-way binding is automatic for form elements
- All HTML elements and SVG elements are available
- Custom components need to be registered with
configureReactive
- Works with forwardRef for ref support