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)} />The machine binding maps handlers (onPress → onPress, onPointerDown → onPressIn),
accessibility props (labelledBy → accessibilityLabelledBy, role → accessibilityRole,
id → nativeID), and accessibility state (expanded → accessibilityState.expanded).
Check out the full mapping here.
Bindings with no RN equivalent (hover, onKeyDown, onWheel) 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 }) => ({ ... })import { 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.