Expo UI が安定版に:1 回のインポートで SwiftUI と Jetpack Compose が使える
SDK 56 から @expo/ui が安定版になりました。一度 import するだけで、iOS では SwiftUI、Android では Jetpack Compose を JS で実装し直すことなく使えます。
日本語
コピー

Expo UI は安定版になり、本番環境で使える。JS の import を 1 回書くだけで、SwiftUI / Compose が支えるネイティブのプリミティブビューが無料で手に入る。差し替え可能なビューも用意されているので、既存のコミュニティパッケージを package.json から削除するのはすぐ済む。

Expo UI のユニバーサルコンポーネント。Android、iOS、Web をカバーする。
SDK 56 以降、@expo/ui を使えば iOS では SwiftUI、Android では Jetpack Compose が使える。JavaScript で作り直したものではなく、本物のネイティブプリミティブだ。
Expo UI はデフォルトの create-expo-app テンプレートに含まれ、Expo Go にもバンドルされているので、プロジェクトを作成したその瞬間から試せる。
このマイルストーンは 3 つの SDK サイクルにわたる反復の結果だ。
-
SDK 53 —— 最初のプロトタイプ。SwiftUI と Jetpack Compose 向けに少数のリーフコンポーネントを用意した。
-
SDK 54 ——
<Host>、SwiftUI modifiers、そしてForm、Listといったコンテナビュー。hot-chocolate のサンプルで、この方式でもアプリを丸ごと作れることが証明された。 -
SDK 55 —— Jetpack Compose サポートが beta に。同時に SwiftUI 側のインターフェースを整理し、コンポーネント、prop、modifier の名前を Apple のドキュメントと 1 対 1 で対応させた。
-
SDK 56 —— Jetpack Compose 側も同じように整理し、両者の上にユニバーサルレイヤーを追加。さらに、長く使われてきたコミュニティパッケージのいくつかにネイティブの 1 対 1 の代替ができた。
それぞれが実際のところどうなのかを順に見ていく。
import 1 回で全プラットフォーム:ユニバーサルコンポーネント
SDK 56 で目に見えて変わるのはユニバーサルレイヤーだ。@expo/ui/swift-ui や @expo/ui/jetpack-compose から import する代わりに @expo/ui から import すれば、内部でプラットフォームに合った実装が自動的に選ばれる:
import { Host, FieldGroup, Row, Switch, Slider, Text, Spacer } from '@expo/ui';
中身は単なるラッパーで、ユニバーサルコンポーネントは iOS では SwiftUI 版を、Android では Compose 版をレンダリングする。間に JS の再実装はないので、どちらの場合も本物のプラットフォームコンポーネントを使っていることになる。レイアウトプリミティブ、テキスト、入力、コントロール、sheet はすべて対象だ:Host、Row、Column、ScrollView など。ユニバーサルコンポーネントの詳細はこちら。
名前の選び方でいくつか挙げておくと、Toggle ではなく Switch、HStack / VStack ではなく Column / Row だ。ユニバーサルレイヤーは、React Native 開発者が毎日打っている名前を優先している。SwiftUI 風の名前が欲しいときは、これまで通り @expo/ui/swift-ui にある。
ユニバーサルコンポーネントの実例:設定画面
FieldGroup は SwiftUI の Form に相当するユニバーサル版で、セクション見出しとフッターを備えたグループ化されたセクション付きリストを、iOS と Android の両方でプラットフォームに合ったスタイルで表示する。下のコードスニペットにある複合コンポーネントに注目してほしい。FieldGroup.Section がセクションを、FieldGroup.SectionFooter がその下のグレーの説明文を表す。
import { useState } from 'react';
import { Button, FieldGroup, Host, Row, Slider, Spacer, Switch, Text } from '@expo/ui';
export default function SettingsScreen() {
const [notifications, setNotifications] = useState(true);
const [sounds, setSounds] = useState(false);
const [brightness, setBrightness] = useState(0.6);
return (
<Host style={{ flex: 1 }}>
<FieldGroup>
<FieldGroup.Section title="Notifications">
<LabeledRow label="Push notifications">
<Switch value={notifications} onValueChange={setNotifications} />
</LabeledRow>
<LabeledRow label="Sounds">
<Switch value={sounds} onValueChange={setSounds} />
</LabeledRow>
<FieldGroup.SectionFooter>
<Text textStyle={{ fontSize: 13, color: '#6c6c70' }}>
Notification previews can expose sensitive content on the lock screen.
</Text>
</FieldGroup.SectionFooter>
</FieldGroup.Section>
<FieldGroup.Section title="Display">
<LabeledRow label="Brightness">
<Slider value={brightness} onValueChange={setBrightness} />
</LabeledRow>
</FieldGroup.Section>
<FieldGroup.Section>
<Row alignment="center" style={{ padding: 12 }}>
<Spacer flexible />
<Button variant="outlined" onPress={() => alert('Signed out')} label="Sign out" />
<Spacer flexible />
</Row>
</FieldGroup.Section>
</FieldGroup>
</Host>
);
}
function LabeledRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<Row alignment="center" spacing={16}>
<Text>{label}</Text>
<Spacer flexible />
{children}
</Row>
);
}
同じコードが 2 つのプラットフォームで動く。iOS では inset-grouped なセクションを持つ SwiftUI の Form、そしてシステムの Switch と Slider が得られる。Android では Material 3 のグループ化リストになる。ページに組み込めば、システム標準の一部のように見える(.ios.tsx / .android.tsx に分ける必要はない)。
プラットフォームパッケージ(@expo/ui/swift-ui、@expo/ui/jetpack-compose)はユニバーサルレイヤーと並んで残っていて、特定の画面で SwiftUI の glassEffect() や Compose の DockedSearchBar が必要なときに使える。同じ Host の中で混ぜても構わない。
ユニバーサルレイヤーについて、早い段階で 2 点:
-
これが最初のバージョンだ。 3 つのプラットフォームすべてでうまく動くユニバーサルコンポーネントもあれば、そうでないものもある。SwiftUI、Compose、Web が常に同じプリミティブを提供しているわけではないからだ。この SDK サイクル中に教えてほしい:どのユニバーサルコンポーネントが役に立ち、どれが立たなかったか、何が足りないか、そして結局
@expo/ui/swift-uiや@expo/ui/jetpack-composeを直接使うことになったのはどこか。 -
Web は実験的だ。 ユニバーサルコンポーネントには Web 実装があるが、品質はネイティブと同じ水準には達していない。皆さんのフィードバックをもとに磨きをかけながら、今後の SDK でもさらに改善していく。
安定したネイティブ API と新機能
2 回の監査を終えた今、見つかる SwiftUI や Compose のサンプルのほとんど(Apple のドキュメント、Google のドキュメント、どこかの Medium の記事、LLM の学習データ)は、Expo UI で動かすまでに検索と置換が 1 回あれば済む。コンポーネント名、prop 名、modifier 名はすべて同じだ。
具体的には、SwiftUI の Toggle と Form は @expo/ui/swift-ui でもそのままの名前で、Compose の LazyColumn は @expo/ui/jetpack-compose でもそのままの名前だ。prop と modifier の命名も同じルールに従っている。
SDK 56 には、Compose レビューに伴う最後の一連のリネームが含まれています。このバージョン以降、API はほぼ安定しています。
レビュー以外にも、このサイクルではいくつかの新機能をリリースしました:
-
拡張性。 SwiftUI と Jetpack Compose には数百のビューとモディファイアがあり、
@expo/uiでラップできる範囲をはるかに超えています。この差を埋めるため、SwiftUI と Jetpack Compose のビューとモディファイアを自分で構築できるようになり、レイアウト、prop、イベントの配管は Expo UI が処理します。詳細は SwiftUI と Jetpack Compose をご覧ください。 -
Material 3 のダイナミックカラー。
useMaterialColorsはシステムテーマに追従する Material 3 のダイナミックカラーを提供します。 -
Material Symbols カタログ。
Iconコンポーネントと@expo/material-symbolsを組み合わせることで、Material Symbols の全カタログに手が届きます。 -
Worklet 駆動のネイティブ状態。 SwiftUI と Jetpack Compose 向けの
useNativeStateを使えば、ちらつきのない UI スレッド駆動のコントロールを構築できます。これについては別途フォローアップ記事を書く予定です。
コミュニティパッケージのドロップイン代替
ほとんどの React Native アプリは、最終的に同じ小さな一群のコミュニティパッケージ——ピッカーやスライダーなどのプラットフォーム標準コンポーネント——を取り込むことになります。どれも独立したネイティブ依存であり、インストールするパッケージが1つ増え、追いかけるリリーススケジュールが1つ増え、SDK アップグレード時に壊れる可能性のある箇所が1つ増えます。SDK 56 では、そのうち7つに対して @expo/ui/community 配下でネイティブの代替品を提供します:
| コミュニティパッケージ | ドロップイン代替 |
|---|---|
| @react-native-community/datetimepicker | @expo/ui/community/datetime-picker |
| @react-native-community/slider | @expo/ui/community/slider |
| react-native-pager-view | @expo/ui/community/pager-view |
| @react-native-picker/picker | @expo/ui/community/picker |
| @react-native-segmented-control/segmented-control | @expo/ui/community/segmented-control |
| @react-native-masked-view/masked-view | @expo/ui/community/masked-view |
| @react-native-menu/menu | @expo/ui/community/menu |
| @gorhom/bottom-sheet | @expo/ui/community/bottom-sheet |
各代替品は、置き換えるパッケージとほぼ API 互換です。ほとんどの移行は import を1行変えるだけで済みますが、Expo UI の基盤が UIKit と Android Views ではなく SwiftUI と Jetpack Compose であるため、一部の prop は異なります:
// Before
import DateTimePicker from '@react-native-community/datetimepicker';
// After
import DateTimePicker from '@expo/ui/community/datetime-picker';
ドロップイン代替の詳細はこちら。
結果として、ネイティブ依存は減り、アップグレード経路は1本になり、基盤は変わらず監査済みのものです。
アプリの各部分に適したツールを選ぶ
react-dom では <div> と <span> を書き、@expo/ui では Column と Row を書きます。理屈は同じです。Expo UI は UI コンポーネントライブラリでも、気の利いたデザインシステムでもありません。iOS と Android にもともとあるネイティブコントロールを React コンポーネントとして公開しているだけです。
ただし Expo UI は道具箱の中の唯一の道具ではありません。Expo がフレームワークとして成り立つ理由の1つは、同じアプリ内で複数のツールを混在させられることです:
-
カスタム UI が必要で、独自のデザインを使いたい場合——ブランドページから完全にカスタムなデザインシステムまで——React Native の
View/Textを使います。これらはスタイル手法に縛られないプリミティブで、CSS 風の props、flexbox レイアウト、NativeWind などのスタイリングライブラリが使えます。 -
ネイティブシステムらしく見える UI が必要な場合——設定画面、modal、picker、sheet など、ユーザーが「システム標準のように見えるはず」と期待する箇所——Expo UI を使います。
Hostの内部では、レイアウトに Yoga flexbox ではなく SwiftUI / Compose のプリミティブ(HStack/VStack、Row/Column)を使います。 -
全部自分で描く必要がある——グラフ、カスタム shader、オーダーメイドのアニメーション——なら react-native-skia。
-
3D と GPU 計算が必要——なら react-native-webgpu と TypeGPU。
-
Web エコシステムにすでに答えがある UI(shadcn など)——なら DOM components を使い、Web のコードを webview の中で動かす。ネイティブ側でもそうだし、Web 側ではそのまま動く。
何より、これらのアプローチはコンポーネントのレベルで組み合わせられる。同じ画面、同じ view tree の中で React Native、Expo UI、Skia、WebGPU / TypeGPU、DOM components を混在させ、汎用でありながら深くネイティブな体験を作れる。
アップデートした showcase アプリ
hot-chocolate を SDK 56 にアップグレードした。このアプリはもともと SwiftUI 版しかなかったが、今は本物のユニバーサルコンポーネントを持ち、Android と Web でも動く。
Apple TV と Android TV
Expo UI は Apple TV と Android TV の両方をサポートするようになった。これは主に Douglas Lowder の仕事によるものだ。ほとんどのコンポーネントと API が使えるが、一部の SwiftUI と Compose のネイティブ API は TV にはそもそも存在しない。ExpoUITV というアプリが、サポートされているほぼすべての API を披露していて、TV でもモバイルでも動く。
コミュニティに感謝
君たちがいなければ、Expo UI が安定版に到達することはなかった。SDK 56 のサイクルは、特にコミュニティの後押しに頼っていた。issue を立て、一緒に API をレビューし、コンポーネントを丸ごと貢献してくれ、こちらが気づいてすらいなかったクラッシュを直してくれた。特に感謝したい:
2hwayoung、AKSHAY JADHAV、Axel、Benjamin Komen、Beto、Christian Wooldridge、Dennis Morello、Dylan、Eliot Gevers、fedeciancaglini、Gregory Moskaliuk、Hugo Extrat、hypnokermit、Ian Berry、Isaiah Hamilton、JeroenG、Joss Mackison、K.Dileepa Thushan Peiris、Kfir Fitousi、kimchi-developer、Leonardo E. Dominguez、Liès、Loic CHOLLIER、Louis、lucabc2000、Petr Chalupa、Pflaumenbaum、Ray、Sam Shubham、Shubh Porwal、starsky-nev、Suvesh Moza、Terijaki、ThiMal、Yousof Abouhalawa、Zhovtonizhko Dmitriy
テストしてくれた人、issue を立ててくれた人、office hours に参加してくれた人、あるいは SDK 56 を進める僕らを横目に Discord で雑談していただけの人にも感謝する。このリリースには君たちの貢献がある。それはあの PR と同じくらい重要だ。
どこから始めるか
npx create-expo-app@latest --template default@sdk-56 —— Expo UI はデフォルトテンプレートに入ったので、Host、Switch、Picker はそのまま import できる。
既存のプロジェクトに上の即席テーブルのいずれかのパッケージがすでに入っているなら、まず import をひとつ差し替えてみるといい。
サンプルアプリを見てみよう:hot-chocolate と ExpoUITV。
issue を立て、PR を送り、Discord に来て話しかけてほしい。安定は完成ではない——土台が次の建設を始められるくらい堅固になったというだけのことだ。
安定版 Expo UI へようこそ。さあ、ネイティブなものを書こう。