Expo UI における Worklet 統合:SwiftUI と Compose の状態を同期制御
Expo UIがreact-native-workletsと統合され、UIスレッド上でSwiftUIとComposeの状態を同期的に制御できるようになりました。JSの往復通信は不要です。
日本語
コピー

SDK 56 で Expo UI が UI runtime worklet とネイティブ統合を果たした。これを可能にしたのが react-native-worklets だ。イベントコールバックを UI スレッド上で同期的に実行し、UI の状態も同期的に制御できるようになった。つまり、こう書ける:
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>
);
}
注意:動かすにはプロジェクトに react-native-reanimated と react-native-worklets が必要だ。
裏で起きていること
2つの仕組みが連携している。
useNativeState は ObservableState を生成する。これはネイティブ側に存在する SharedObject で、SwiftUI と Compose の両方から監視される。内部的には iOS では ObservableObject、Android では MutableState に対応する。
Worklet コールバック(onTextChange など)は、ネイティブビューがイベントを発火したときに UI スレッド上で直接実行される。
この2つが組み合わさると、TextField でキーを1回叩くたびに共有された text の状態が更新され、worklet が走り、SwiftUI と Compose が再レンダリングされる。JS スレッドに飛ぶことは一切ない。
SwiftUI を書いたことがある人なら見覚えがあるはずだ。上の TS コードはほぼ1対1でこう対応する:
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 が @State の役割を果たし、text={text} が TextField(text: $text) に相当し、worklet onTextChange が .onChange(of:) に対応する。Compose で mutableStateOf と onValueChange を使っても構造は同じだ。
ちらつきのない同期入力マスク
この仕組みの最も直接的な恩恵は、入力マスクがようやくまともに動くようになったことだ。worklet はキー入力が届いた同じフレーム内で text.value を書き換えられる。ユーザーがマスク前の文字を見ることはなく、JS スレッドを経由する非同期の往復も発生しない。
次のクレジットカード入力欄は、入力時に UI スレッド上で 4242424242424242 を 4242 4242 4242 4242 に整形する:
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 — expo.dev で視聴
同じパターンは電話番号、日付、郵便番号、通貨、その他表示テキストを生のキー入力と変えたいあらゆる場面に使える。
Worklet 統合が重要な理由
Worklet 統合によって Expo UI はよりネイティブに近い体験を提供できるようになり、既存の非同期の選択肢に加えて同期という選択肢が生まれた。インタラクションの要件に応じて選べばいい。
入力マスクはその一例にすぎない。同じ「ネイティブ状態 + worklet」のパターンを使えば、Expo UI はネイティブ状態に基づく SwiftUI や Compose の API をさらに多く React Native に持ち込める。
今後どう発展していくのか楽しみだ。
試してみる
Worklet は @expo/ui/swift-ui と @expo/ui/jetpack-compose の両方で使え、すでに SDK 56 に入っている。TextInput は最初に対応したコンポーネントの1つで、今後のバージョンでより多くのフォームコントロールが同期コールバックを備える予定だ。