Reference version

This is documentation for the next SDK version. For up-to-date documentation, see the latest version (SDK 56).

useNativeState

A React hook that creates observable state shared between JavaScript and native Jetpack Compose views.

Android
Included in Expo Go

For the complete documentation index, see llms.txt. Use this file to discover all available pages.

useNativeState returns an ObservableState that maps to a Compose MutableState on the native side, so reads and writes to .value are tracked directly by Compose without going through the React render cycle. This lets you update the native view synchronously from a worklet on the UI thread.

Installation

Terminal
npx expo install @expo/ui

If you are installing this in an existing React Native app, make sure to install expo in your project.

Usage

Note: Using worklets requires installing react-native-reanimated and react-native-worklets in your project. useNativeState itself works without them, but the synchronous UI-thread updates shown below depend on the worklet runtime.

The example below masks a phone number as the user types. The formatting and the writes to maskedPhone.value (text) and selection.value (cursor position) all happen synchronously on the UI thread, so there is no flicker between the typed value and the masked value.

WorkletPhoneMaskExample.tsx
import { Host, TextField, Text as ComposeText, useNativeState } from '@expo/ui/jetpack-compose'; import { fillMaxWidth } from '@expo/ui/jetpack-compose/modifiers'; import { useEffectEvent } from 'react'; export default function WorkletPhoneMaskExample() { const maskedPhone = useNativeState(''); const selection = useNativeState({ start: 0, end: 0 }); const handleValueChange = useEffectEvent((v: string) => { 'worklet'; const digits = v.replace(/\D/g, '').slice(0, 10); let formatted: string; if (digits.length === 0) { formatted = ''; } else if (digits.length <= 3) { formatted = digits; } else if (digits.length <= 6) { formatted = `(${digits.slice(0, 3)}) ${digits.slice(3)}`; } else { formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; } if (formatted !== v) { maskedPhone.value = formatted; // Snaps to end for demo. Real masks need smarter cursor handling. selection.value = { start: formatted.length, end: formatted.length }; } }); return ( <Host matchContents> <TextField value={maskedPhone} selection={selection} keyboardOptions={{ keyboardType: 'phone' }} modifiers={[fillMaxWidth()]} onValueChange={handleValueChange}> <TextField.Placeholder> <ComposeText>(555) 123-4567</ComposeText> </TextField.Placeholder> </TextField> </Host> ); }

API

import { useNativeState } from '@expo/ui/jetpack-compose';

Hooks

useNativeState(initialValue)

Android
ParameterType
initialValueT

Creates an observable native state that is automatically cleaned up when the component unmounts. initialValue is captured once on the first render

Returns:
ObservableState<T>

Types

ObservableState

Android

Observable state shared between JavaScript and native views (Jetpack Compose on Android and SwiftUI on iOS).

Type: SharedObject extended by:

PropertyTypeDescription
onChange[listener] | null

A single listener invoked on the native UI runtime whenever the value changes (after iOS didSet and Android's setter). Assigning replaces the previous listener; assign null to clear. The initial value does not fire onChange.

The callback must be a worklet so it can run synchronously on the UI thread. Attach it inside useEffect and clear it in the cleanup so the listener lifecycle matches the component lifecycle.

Example

const state = useNativeState(0); useEffect(() => { state.onChange = (value) => { 'worklet'; console.log('changed to', value); }; return () => { state.onChange = null; }; }, []);
valueT

The current value.

Writes from a UI worklet are synchronous and immediately readable. Writes from the JS thread are scheduled to the UI thread asynchronously, the new value is not readable until the update has been applied. Prefer writing from a worklet when you need synchronous updates