React Native
npm install @dunky-dev/native-state-machineyarn add @dunky-dev/native-state-machinepnpm add @dunky-dev/native-state-machinebun add @dunky-dev/native-state-machineThe React Native package is the same thin edge layer as React — same lifecycle hook, same ComponentEffect model — with one difference: normalize translates bindings to React Native props instead of DOM/ARIA props. The machine itself is unchanged. The same config and connect function run on both platforms.
useMachine, useSelector, ComponentEffect
Section titled “useMachine, useSelector, ComponentEffect”Re-exported directly from @dunky-dev/react-state-machine — identical on both platforms because React Native uses the same React renderer. See the React page for full docs on these.
import { useMachine, useSelector, type ComponentEffect } from '@dunky-dev/native-state-machine'normalize — bindings → React Native props
Section titled “normalize — bindings → React Native props”The RN normalize translates the same agnostic bindings to React Native’s prop vocabulary:
import { normalize } from '@dunky-dev/native-state-machine';<Pressable {...normalize(api.triggerProps)} />| Binding | React Native prop |
|---|---|
onPress | onPress |
onPointerDown | onPressIn |
onFocus / onBlur | onFocus / onBlur |
onPointerEnter/Leave/Move | dropped — no RN analog; use focus or long-press instead |
onKeyDown | dropped — no RN analog |
describedBy / labelledBy | accessibilityLabelledBy |
role | accessibilityRole |
id | nativeID |
expanded / selected / disabled / hidden | accessibilityState.expanded/selected/disabled/hidden |
focusable | focusable |
Bindings with no RN equivalent are silently dropped rather than passed as invalid props.
mergeProps
Section titled “mergeProps”RN-aware merge: handlers are chained, style is merged as a [consumerStyle, libraryStyle] array (RN accepts style arrays natively), everything else the component wins.
import { mergeProps, normalize } from '@dunky-dev/native-state-machine';<Pressable {...mergeProps(props, normalize(api.triggerProps))} />ComponentEffect on React Native
Section titled “ComponentEffect on React Native”Platform effects work the same way — a [setup/teardown, depPropNames] tuple passed to useMachine. The platform API changes, not the shape:
import { BackHandler } from 'react-native'import type { ComponentEffect } from '@dunky-dev/native-state-machine'import type { DialogMachine, DialogProps } from './machine'
type Effect = ComponentEffect<DialogMachine, DialogProps>
const onBackButton: Effect = [ (machine, props) => { if (!props.closeOnBackButton) return const handler = BackHandler.addEventListener('hardwareBackPress', () => { machine.send({ type: 'close' }) return true // prevent default back navigation }) return () => handler.remove() }, ['closeOnBackButton'],]
export const dialogEffects = [onBackButton]The machine receives the same send({ type: 'close' }) as the web version. The machine is unchanged; only the transport differs.
Same machine, two targets
Section titled “Same machine, two targets”The machine config and connect function are written once and shared across platforms:
// shared/disclosure.ts — imported by both web and native componentsexport const createDisclosureConfig = (props: DisclosureProps) => ({ ... })export const connectDisclosure = ({ state, send }) => ({ ... })// web — Disclosure.tsximport { useMachine, normalize } from '@dunky-dev/react-state-machine'import { createDisclosureConfig, connectDisclosure } from '../shared/disclosure'import { disclosureEffects } from './effects' // DOM keydown listener
// native — Disclosure.native.tsximport { useMachine, normalize } from '@dunky-dev/native-state-machine'import { createDisclosureConfig, connectDisclosure } from '../shared/disclosure'import { disclosureEffects } from './effects' // BackHandlerThe machine never changes. Only normalize and the ComponentEffect list differ between targets.