概要
SDK 56 で、Expo UI は UI ランタイムの 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 にマッピングされます。
onTextChange のような Worklet コールバックは、ネイティブビューがイベントを発火したときに直接 UI スレッド上で実行されます。
これらが組み合わさることで、TextField における各キー入力が共有テキスト状態を更新し、worklet を実行し、SwiftUI と Compose が再レンダリングする、という流れがすべて JS スレッドに切り替わることなく(hop することなく)実現されます。SwiftUI を書いたことがあれば、これが馴染み深い感覚であるはずです。
上の TS コードはほぼ 1:1 で次の SwiftUI コードに対応します:
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 スレッドへの非同期往復も発生しません。例えば、カード番号フィールドで 4242424242424242 をユーザーが入力すると、タイピング中に UI スレッド上で 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>
)
}
同じパターンは電話番号、日付、郵便番号、通貨など、表示テキストと実際の生入力が異なる必要があるあらゆるケースに適用できます。
なぜ worklet 統合が重要か
Worklet 統合により、Expo UI はよりネイティブに近い UX を提供でき、既存の非同期的アプローチに対して同期的な代替手段を得られます。これにより、やり取りの要件に応じて手法を選べます。入力マスキングは一例に過ぎません。ネイティブ状態 + worklet の組み合わせは、SwiftUI や Compose ベースの多くのネイティブ状態 API を React Native に導入する土台にもなります。今後の展開が楽しみです。
試してみてください
- Worklet サポートは
@expo/ui/swift-ui と @expo/ui/jetpack-compose の両方で動作し、SDK 56 に導入されています。
TextInput は最初に対応したコンポーネントの一つです。今後のリリースでより多くのフォームコントロールが同期コールバックを持つようになることを期待してください。