Package @reatom/preact
Reatom adapter for Preact.
Installation
npm i @reatom/preactRead the handbook first for production usage.
Using Atoms in JSX
The primary way to use Reatom atoms in Preact JSX is with toPreact, which converts atoms to Preact Signals.
toPreact
Converts a Reatom atom to a Preact signal. The signal is lazily connected to the atom - it subscribes only when the signal itself gets subscribers.
import { atom, computed } from '@reatom/core'import { toPreact } from '@reatom/preact'
const countAtom = atom(0, 'count')
// In Preact component - signal is reactiveconst Counter = () => <div>{toPreact(countAtom)}</div>
// Direct JSX binding - can be called repeatedly in renderconst inputAtom = atom('', 'input')const Input = () => <input value={toPreact(inputAtom)} />
// With computed atoms (returns ReadonlySignal)const doubleAtom = computed(() => countAtom() * 2, 'double')const doubleSignal = toPreact(doubleAtom) // ReadonlySignal<number>withPreact
Extension that adds a .preact property with a Preact signal synchronized with the target atom.
import { atom, computed } from '@reatom/core'import { withPreact } from '@reatom/preact'
const countAtom = atom(0, 'count').extend(withPreact())
// In Preact component:const Counter = () => <div>{countAtom.preact}</div>
// Writable - setting signal value updates the atomcountAtom.preact.value = 5
// Computed atom - signal is read-onlyconst doubleAtom = computed(() => countAtom() * 2, 'double').extend(withPreact())// doubleAtom.preact is ReadonlySignal<number>You can setup .preact accessor to ALL atoms automatically by using addGlobalExtension. Do this in a “setup” file and import it before any other imports:
import { addGlobalExtension } from '@reatom/core'import { withPreact, type PreactExt, type PreactReadonlyExt } from '@reatom/preact'
addGlobalExtension(withPreact())
declare module '@reatom/core' { interface Atom<State> extends PreactExt<State> {} interface Computed<State> extends PreactReadonlyExt<State> {}}Binding Atoms to Components
Reatom offers powerful ways to integrate state management directly into your Preact components, ensuring reactivity and proper lifecycle management. It is an alternative to Preact signals, if you want to stick only to one reactive primitive.
Automatic Tracking
The easiest way to use Reatom with Preact is to enable automatic tracking. This patches Preact’s options.vnode hook to automatically wrap all function components with reatomComponent, so you can use atoms directly without any manual wrapping.
Option 1: Import for side effect (recommended)
Simply import the /auto entry point once at the top of your application:
import '@reatom/preact/auto'Option 2: Manual setup
If you need more control over when auto-tracking is installed:
import { installAutoTracking } from '@reatom/preact'
installAutoTracking()After enabling auto-tracking, all your components become reactive:
import '@reatom/preact/auto'import { atom, wrap } from '@reatom/core'
const count = atom(0, 'count')
// No need to wrap with reatomComponent - it's automatic!const Counter = () => ( <div> Count: {count()} <button onClick={wrap(() => count.set((c) => c + 1))}>+</button> </div>)reatomComponent
The primary API to bind atoms and actions to a component’s lifetime is reatomComponent. It wraps your regular Preact component function, placing it within a Reatom reactive context.
Features:
- Reactive Reads: Simply call an atom (
myAtom()) within the component function to read its value and subscribe to updates. The component will automatically re-render when the atom changes. - Standard Preact: Use any other Preact hooks (
useState,useEffect, etc.), accept props, and return any validComponentChildrenas usual. - Context Preservation: Event handlers should be wrapped with
wrap()(e.g.,onClick={wrap(myAction)}) to preserve the reactive context, especially for async operations or actions updating state. - No Hooks Rules for Atoms: Call and subscribe to atoms conditionally within your render logic without violating Preact’s rules of hooks.
- Automatic Cleanup: Integrates with Reatom’s abort context. Effects or async operations triggered from within the component (using
wrapor implicitly by actions) are automatically aborted if the component unmounts before completion, preventing race conditions and memory leaks.
import { atom, wrap } from '@reatom/core'import { reatomComponent } from '@reatom/preact'
export const page = atom(0, 'page').extend((target) => ({ next: () => target((state) => state + 1), prev: () => target((state) => Math.max(0, state - 1)),}))
// Simple component reading and updating global stateexport const Paging = reatomComponent( () => ( <span> <button onClick={wrap(page.prev)}>prev</button> {/* Use wrap */} {page()} {/* Read atom value */} <button onClick={wrap(page.next)}>next</button> {/* Use wrap */} </span> ), 'Paging',) // Naming the component is crucial for debugging!
// Component accepting props (including atoms)type CounterProps = { label: string count: Atom<number> increment: Action<[], number>}
export const Counter = reatomComponent<CounterProps>( ({ label, count, increment }) => ( <div> {label}: {count()} <button onClick={wrap(increment)}> + </button> </div> ), 'Counter',)
// Conditional rendering based on atom valuesexport const SomeList = reatomComponent( () => isLoading() ? ( // Read atom conditionally <span>Loading...</span> ) : ( <ul> {list().map( ( el, // Read another atom ) => ( <li key={el.id}>{el.text}</li> ), )} </ul> ), 'SomeList',)Do not forget to put the component name to the second argument, it will increase your feature debug experience a lot!
Unmount Behavior
A key feature of reatomComponent is its integration with Reatom’s abort mechanism. When a reatomComponent unmounts:
- Its associated reactive context is aborted.
- Any pending async operations initiated within that context (e.g.,
await wrap(fetch(...)),await wrap(sleep(...))) are automatically cancelled. - Any active
effectprimitives created within its context are automatically cleaned up.
This robust cleanup prevents common issues like trying to update state on unmounted components and avoids memory leaks from lingering subscriptions or timers. If you need an operation to survive component unmount (e.g., analytics), use spawn from the core package.
reatomFactoryComponent (Recommended for Local State/Effects)
While reatomComponent is great for reading atoms state, reatomFactoryComponent is the recommended pattern for components that need their own local, encapsulated state and side effects.
It separates the component logic into two parts:
- Factory Function: Runs once when the component instance is created. This is where you define local atoms, actions, and effects specific to this component instance. It receives the component’s initial props.
- Render Function: Runs on every render, just like a regular Preact component function. It has access to the atoms and actions created in the factory scope and the current props.
Benefits:
- True Encapsulation: Local state and effects are tied to the component instance, not shared globally.
- Lifecycle Management: The factory scope provides a natural place for setup logic.
- Perfect for
effect:effectprimitives created in the factory are automatically cleaned up when the component unmounts, making it ideal for managing local subscriptions, timers, animations, etc. - Stable References: Atoms, actions or any other functions created in the factory have stable references across renders.
import { atom, action, effect, wrap, sleep } from '@reatom/core'import { reatomFactoryComponent } from '@reatom/preact'
// Example: A self-contained counter componentconst Counter = reatomFactoryComponent<{ initialCount: number; step?: number }>( // 1. Factory Function (runs once per instance) (initProps) => { // Note that the props will not change in this initialization scope. const step = initProps.step ?? 1 // Create local atom specific to this Counter instance const count = atom(initProps.initialCount, 'localCount') // Create local action const increment = action(() => count((c) => c + step), 'increment') const decrement = action(() => count((c) => c - step), 'decrement')
// Example: Log changes (effect cleans up automatically) effect(() => { const currentCount = count() console.log(`Counter ${initProps.initialCount} changed to:`, currentCount) // Cleanup function (optional, runs before next effect run or on unmount) return () => console.log( `Counter ${initProps.initialCount} leaving state:`, currentCount, ) }, 'logEffect')
// Return the render function return (props) => ( <div> Count (Initial: {props.initialCount}, Step: {props.step ?? 1}):{' '} {count()} <button onClick={wrap(decrement)}>-</button> <button onClick={wrap(increment)}>+</button> </div> ) }, 'Counter', // Name the factory component!)
// Usage:// <Counter initialCount={10} />// <Counter initialCount={0} step={5} />Example: Using effect for Auto-Cleaning
reatomFactoryComponent combined with effect is excellent for managing resources that need cleanup. It is more powerful and precise primitive than useEffect, as it isn’t coupled with rerenders.
import { atom, effect, wrap, sleep } from '@reatom/core'import { reatomFactoryComponent } from '@reatom/preact'
const IntervalLogger = reatomFactoryComponent<{ intervalMs: number }>( ({ intervalMs }) => { const tick = atom(0, 'tick')
// This effect runs a timer and cleans it up automatically on unmount effect(async () => { while (true) { // sleep respects the abort context await wrap(sleep(intervalMs)) tick((t) => t + 1) } }, 'intervalEffect')
return (props) => ( <div> Interval ({props.intervalMs}ms) Ticks: {tick()} </div> ) }, 'IntervalLogger',)
// Usage:// <IntervalLogger intervalMs={1000} />// When this component unmounts, the interval stops automatically.reatomFactoryComponent provides a robust and elegant way to build stateful, effectful components with automatic lifecycle management, leveraging the power of Reatom’s core primitives like atom and effect.
Setup context
Optionally, you need to set up the main context once and wrap your application in a provider at the top level. This is required only if you have called clearStack() (recommended).
import { clearStack, frame } from '@reatom/core'import { reatomContext } from '@reatom/preact'import { Main } from './path/to/an/Main'
clearStack()
const rootFrame = frame()
export const App = () => ( <reatomContext.Provider value={rootFrame}> <Main /> </reatomContext.Provider>)Form Binding
bindField
Helper function to bind a Reatom field atom to form inputs in Preact.
import { field } from '@reatom/core'import { reatomComponent } from '@reatom/preact'import { bindField } from '@reatom/preact'
const nameField = field('', 'name')
const NameInput = reatomComponent( () => <input {...bindField(nameField)} />, 'NameInput',)The bindField function returns an object with:
value/checked- the current field value (usescheckedfor boolean fields)onChange- handler for value changesonBlur- handler for blur events (triggersfield.focus.out())onFocus- handler for focus events (triggersfield.focus.in())error- validation error message if any
Variables
reatomContext
reatomContext:
Context<Frame<any,any[],any> |null>
Defined in: reatomComponent.ts:23
Functions
_getComponentDebugName()
_getComponentDebugName(
fallback?):string
Defined in: reatomComponent.ts:18
Parameters
fallback?
string
Returns
string
bindField()
bindField<
T>(field):object
Defined in: bindField.ts:6
Type Parameters
T
T = any
Parameters
field
FieldAtom<any, T>
Returns
object
checked
checked:
Textendsboolean?boolean:undefined
error
error:
string|undefined
onBlur()
onBlur: () =>
void
Returns
void
onChange()
onChange: (
value) =>void
Parameters
value
T | { currentTarget: T extends boolean ? object : object; } | Event
Returns
void
onFocus()
onFocus: () =>
void
Returns
void
value
value:
T
installAutoTracking()
installAutoTracking():
void
Defined in: installAutoTracking.ts:5
Returns
void
isSuspense()
isSuspense(
thing):boolean
Defined in: reatomComponent.ts:70
Parameters
thing
unknown
Returns
boolean
reatomComponent()
reatomComponent<
Props>(UserComponent,name?):ComponentClass<Props>
Defined in: reatomComponent.ts:79
Type Parameters
Props
Props extends Rec = { }
Parameters
UserComponent
ComponentType<Props>
name?
string
Returns
ComponentClass<Props>
reatomFactoryComponent()
reatomFactoryComponent<
Props>(init,name?):ComponentClass<Props>
Defined in: reatomComponent.ts:149
Type Parameters
Props
Props extends Rec = { }
Parameters
init
(initProps) => (props) => ComponentChildren
name?
string
Returns
ComponentClass<Props>
useFrame()
useFrame():
Frame
Defined in: reatomComponent.ts:25
Returns
Frame
useWrap()
useWrap<
Params,Payload>(callback,name?): (…params) =>Payload
Defined in: reatomComponent.ts:37
Type Parameters
Params
Params extends any[]
Payload
Payload
Parameters
callback
(…params) => Payload
name?
string
Returns
(…
params):Payload
Parameters
params
…Params
Returns
Payload