---
title: 'Worklet integration in Expo UI: synchronously controlling SwiftUI and Compose state'
authors: Nishan Bende
published: May 27, 2026
categories: Product, Development, React Native
tags: Expo UI
---

[In SDK 56, Expo UI](https://expo.dev/blog/expo-ui-stable-sdk-56) comes with first-class integration with UI runtime worklets. Thanks to [`react-native-worklets`](https://github.com/software-mansion/react-native-worklets). What that means is that we can run event callbacks synchronously on UI thread and control UI state synchronously. It means you can do this:

```typescript
import { Host, TextInput, useNativeState } from '@expo/ui';

export default function Screen() {
  const value = useNativeState('');

  return (
    <Host matchContents>
      <TextInput
        value={value}
        placeholder="Type something"
        onChangeText={(value) => {
          'worklet';
          // Runs synchronously on the UI thread, on every keystroke.
          console.log('[UI thread] typed:', value);
        }}
      />
    </Host>
  );
}
```

> Note: you'll need `react-native-reanimated` and `react-native-worklets` installed in your project for this to work.

## What's actually happening

Two pieces work together here:

1. **`useNativeState`** creates an `ObservableState`, a [`SharedObject`](https://docs.expo.dev/modules/shared-objects/) that lives on native and is observed by both SwiftUI and Compose. Under the hood it maps to an [`ObservableObject`](https://developer.apple.com/documentation/combine/observableobject) on iOS and a [`MutableState`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/MutableState) on Android.
2. **Worklet callbacks** like `onTextChange` are executed directly on the UI thread when the native view fires its event.

Together, this means each keystroke in the `TextField` updates the shared `text` state, runs your worklet, and lets SwiftUI and Compose re-render, all without ever hopping to the JS thread.

If you've written SwiftUI, this should feel familiar. The TS code above maps almost 1:1 to:

```swift
struct Screen: View {
  @State var text = ""

  var body: some View {
    TextField("Type something", text: $text)
      .onChange(of: text) { _, newValue in
        print("[UI thread] typed:", newValue)
      }
  }
}
```

`useNativeState` plays the role of `@State`, `text={text}` is the equivalent of `TextField(text: $text)`, and the worklet `onTextChange` mirrors `.onChange(of:)`. The same shape works on Compose with `mutableStateOf` and `onValueChange`.

## Flicker-free synchronous input masking

The most immediate payoff of this is **input masking that just works**. Because the worklet can rewrite `text.value` on the same frame the keystroke arrives, the user never sees the unmasked character, there's no asynchronous round-trip through the JS thread.

Here's a credit card field that formats `4242424242424242` into `4242 4242 4242 4242` as the user types, on the UI thread:

```typescript
import { Host, TextInput, useNativeState } from '@expo/ui/swift-ui';

export default function CardNumberField() {
  const value = useNativeState('');

  return (
    <Host matchContents>
      <TextInput
        value={value}
        placeholder="Card number"
        onChangeText={(value) => {
          'worklet';
          const digits = value.replace(/\\D/g, '').slice(0, 16);
          const masked = digits.replace(/(.{4})/g, '$1 ').trim();
          text.value = masked;
        }}
      />
    </Host>
  );
}
```

[blog_video_16x9](https://player.mediadelivery.net/embed/615344/cc4e1762-c170-4089-a24e-06865adac6e9)

The same pattern works for phone numbers, dates, postal codes, currency, anything where the displayed text needs to differ from the raw keystrokes.

## Why worklet integration matters

Worklet integration lets Expo UI deliver a more native-feeling UX, and gives you a synchronous alternative alongside the existing async one, so you can pick based on what the interaction needs.

Input masking is just one of the usecases. The same native state + worklet pattern allows Expo UI to bring a lot more native state based SwiftUI and Compose APIs to React Native.

We're excited to see where this goes next.

## Try it

Worklet support works on both `@expo/ui/swift-ui` and `@expo/ui/jetpack-compose`, and is [landed in SDK 56](https://expo.dev/changelog/sdk-56). `TextInput` is one of the first components wired up, expect more form controls to gain sync callbacks in upcoming releases.