【Expo】SDK 56で安定版になったExpoUI

はじめに
この記事の概要
こんにちは、株式会社TOKOSのスギタです!
今回は、React Native向けのフレームワークであるExpoの最新版「SDK 56」におけるExpoUI(パッケージ名@expo/ui)について解説します。
2026年5月21日にリリースされたExpo「SDK 56」では、待望だった@expo/uiがついに安定版で利用可能になりました。
「SDK 53」でアルファ版として登場してから数バージョンを経て、「SDK 56」ではデフォルトのcreate-expo-appテンプレートに同梱され、Expo Goでも利用できるようになっています。
本記事では、「SDK 55」までの位置づけと「SDK 56」での変化を整理しつつ、ExpoUIの概要が把握できるようにまとめていきます。
Expo SDK 56 - Expo Changelog
Check out new updates and improvements to Expo and EAS from the Expo team.
expo.dev対象読者
- フロントエンドエンジニアの方
- React Native / Expoでアプリ開発をしている方
.ios.tsx/.android.tsxでの書き分けに課題を感じている方
この記事で扱う内容、扱わない内容
- ExpoUIの概要
- SDK 56でのExpoUIの主要な変更点
- Universal Componentsの紹介
この記事で扱わない内容は下記です。
- React Native / Expoの基本的な使い方
- SwiftUI / Jetpack Composeの基本的な使い方
ExpoUIの概要
@expo/uiは、iOSではSwiftUI、AndroidではJetpack Composeを直接使ってネイティブUIを構築するためのコンポーネント群です。
「React NativeのViewをネイティブ風に見せる」のではなく、本物のSwiftUI / ComposeをそのままReactからマウントするのがポイントです。
提供される名前空間は大きく3つに分かれています。
@expo/ui/swift-ui: iOS向け、SwiftUIベース@expo/ui/jetpack-compose: Android向け、Material3Composeベース@expo/ui/...(Universal): iOS/Android/Webで共通に使えるクロスプラットフォームAPI
「SDK 56」から新しく追加されたのが、@expo/ui/...(Universal)です!
SDK 55 までのExpoUIの位置づけ
「SDK 56」の変更を理解するために、「SDK 55」時点でのExpoUIの状態を簡単に振り返ります。
- SwiftUI API:「SDK 55」でベータ。SwiftUI寄りに命名を揃える破壊的変更が入った段階
- Jetpack Compose API:「SDK 55」でアルファ版からベータ版に昇格。Material3コンポーネントが一気に追加された段階
- Universalコンポーネント:未提供。クロスプラットフォームUIは引き続き
.ios.tsx/.android.tsxで書き分けるか、React Nativeのコンポーネントを使う必要があった段階 - Expo Go:ExpoUIは、Expo Goで試せなかった段階
「SDK 55」時点ではベータ版ということもあり、プロダクションへの本格採用には踏ん切りがつきにくいフェーズだったと理解しています!
SDK 56 でのExpoUIの主要な変更点
公式Changelogでは、「SDK 56」におけるExpoUIの柱として以下の3点が挙げられています。
- Universal Componentsの新登場
- ネイティブAPI(SwiftUI / Compose)の安定化
- コミュニティライブラリからimport文の書き換えのみで利用可能
順に見ていきます。
1. Universal Components
「SDK 56」の目玉は何と言ってもプラットフォームをまたいで動く共通コンポーネント群(Universal Components)の追加です。
同じコンポーネントでも、裏側はiOS・Android・Webで別々の仕組みになります。
対応表は下記です。
| 動かす環境 | Universalの裏で動くもの |
|---|---|
| iPhone / iPad(iOS) | SwiftUI(細かいAPIが要るときは@expo/ui/swift-ui) |
| Android 端末 | Jetpack Compose(同様に@expo/ui/jetpack-compose) |
| ブラウザ(Web) | ReactがDOMへ描く。react-domかreact-native-webのどちらかを使う想定になります |
提供されるのはレイアウト・テキスト・入力・コントロール・シート系の最小コンポーネントです。
レイアウト系にはHost / Row / Column / Spacer / ScrollViewがあります。
表示系にはText / Icon、入力系にはTextInput / Button / Switch / Slider / Checkboxが含まれます。
加えてBottomSheetやFieldGroupといった構成系コンポーネントも提供されます。
これにより、これまで.ios.tsxと.android.tsxに分けて書いていたUIを単一ファイルに統合できるケースが増えます。
「全部の画面をUniversalで済ます」ではなく、「共通化できる部分はUniversal」、「こだわりたい画面はプラットフォーム別API」という使い分けが現実的な落としどころになりそうです!
2. SwiftUI / Jetpack Compose APIの安定化
「SDK 53」から「SDK 56」まで破壊的変更を経て、ネイティブ側APIがついに安定版になりました。
実用面で効くポイントは次の3つです。
2-1. カスタムView / Modifierの拡張
自前のSwiftUI ViewやCompose ModifierをExpoUIへ差し込めるようになりました。
レイアウト同期・props・イベントはExpoUI側が面倒を見てくれるので、「あのネイティブ部品だけ足りない」というケースを自前で埋められます。
2-2. Material 3 Dynamic ColorsとMaterial Symbols
useMaterialColorsでシステムテーマに追従するMaterial 3の動的カラーが取れるようになり、Icon + @expo/material-symbolsでMaterial Symbolsのフルカタログが使えます。
import { Icon, useMaterialColors } from "@expo/ui/jetpack-compose"
export function FavoriteIcon() {
const colors = useMaterialColors()
return <Icon name="favorite" tintColor={colors.primary} size={24} />
}2-3. ネイティブ状態と同期
useNativeStateなら、文字などの状態を端末側に残したままJavaScriptから扱えます。
WorkletCallbackを使えば、入力イベントにぴったり寄せて処理できます。
結果として、従来の制御入力で出やすかった入力と表示のズレやちらつきを抑えやすくなります。
import { Host, TextInput } from "@expo/ui"
import { useNativeState } from "@expo/ui/swift-ui"
export function NativeStateExample() {
const [text, setText] = useNativeState("")
return (
<Host style={{ flex: 1 }}>
<TextInput value={text} onValueChange={setText} />
</Host>
)
}3. コミュニティライブラリからimport文の書き換えのみで利用可能
ExpoUIがネイティブプリミティブをカバーするようになったことで、既存のコミュニティライブラリと役割の重なる部分も出てきました。
「SDK 56」ではimportを差し替えるだけで移行できます。
対象は以下のとおりです。
@react-native-segmented-control/segmented-control@react-native-picker/picker@react-native-community/datetimepicker@react-native-masked-view/masked-view@gorhom/bottom-sheet
たとえばDateTimePickerは以下のように書き換えられます。
// 旧
import DateTimePicker from "@react-native-community/datetimepicker"
// 新
import DateTimePicker from "@expo/ui/community/datetime-picker"Universal Componentsの紹介
基本のお作法:Hostでラップする
Universal Componentsを使うときは、ツリーの先頭をHostで包みます。
HostがiOSならSwiftUI、AndroidならJetpack Composeへのつなぎ役になります。
中身のコンポーネントは必ずこのHostの内側に書き、importは@expo/ui直下から取ります。
Host
A cross-platform Host component that wraps universal @expo/ui content.
docs.expo.devimport { Host, Column, Button, Text } from "@expo/ui"
export default function Example() {
return (
// ツリーのトップでHostを使用
<Host style={{ flex: 1 }}>
<Column spacing={12} alignment="center">
<Text>Hello, world!</Text>
<Button label="Press me" onPress={() => alert("Pressed")} />
</Column>
</Host>
)
}ここから個人的に注目している、FieldGroup / BottomSheet / TextInput(+ レイアウト系)の使い方を見ていきます。
FieldGroup
FieldGroupはiOSの設定アプリのような、セクション分けされたグループ型のフォームをそのまま再現してくれるコンポーネントです。
SwiftUIのFormに相当する挙動を持ち、AndroidではJetpack Compose側で同等の見た目に変換されます。
サブコンポーネントとしてFieldGroup.Section / FieldGroup.SectionHeader / FieldGroup.SectionFooterが用意されています。
FieldGroup
A scrollable container of grouped settings-style rows.
docs.expo.devimport { useState } from "react"
import { Host, FieldGroup, Switch, Text } from "@expo/ui"
export default function SettingsScreen() {
const [push, setPush] = useState(true)
const [email, setEmail] = useState(false)
return (
<Host style={{ flex: 1 }}>
<FieldGroup>
<FieldGroup.Section title="通知">
<Switch label="プッシュ通知" value={push} onValueChange={setPush} />
<Switch label="メール通知" value={email} onValueChange={setEmail} />
</FieldGroup.Section>
<FieldGroup.Section title="アプリ情報">
<Text>バージョン 1.0.0</Text>
</FieldGroup.Section>
</FieldGroup>
</Host>
)
}titleを渡せば見出しが、<FieldGroup.SectionFooter>を入れれば説明文(よくある「この設定は〜」みたいなグレーの注釈)がSwiftUI/Compose標準のスタイルで描画されます。
設定画面・プロファイル画面・各種フォームなど、これまで自前で組んでいたありがちな画面の実装コストが大幅に下がるのがこのコンポーネント最大の旨味です。
BottomSheet
BottomSheetは画面下部からスライドアップしてくるモーダルシートです。@gorhom/bottom-sheetからimport文を書き換えるだけで利用可能になっており、isPresentedで表示を制御するReact的なAPIになっています。
BottomSheet
A modal sheet that slides up from the bottom of the screen.
docs.expo.devimport { useState } from "react"
import { Host, Column, Button, BottomSheet, Text } from "@expo/ui"
export default function Example() {
const [isPresented, setIsPresented] = useState(false)
return (
<Host style={{ flex: 1 }}>
<Button label="シートを開く" onPress={() => setIsPresented(true)} />
<BottomSheet isPresented={isPresented} onDismiss={() => setIsPresented(false)}>
<Column spacing={12}>
<Text textStyle={{ fontSize: 18, fontWeight: "700" }}>シートの内容</Text>
<Text>下にスワイプするか、オーバーレイをタップで閉じます。</Text>
<Button label="閉じる" onPress={() => setIsPresented(false)} />
</Column>
</BottomSheet>
</Host>
)
}TextInput + WorkletCallback
「SDK 56」からTextInputは、valueにネイティブ側の状態を載せられるようになりました。
iOSとAndroidの両方です。
これまでReactのuseStateで入力文字を管理すると、valueが変わるたびに「端末とJavaScript」を行き来します。
その往復が遅いと、入力と表示のあいだにちらつきが起きやすくなっていました。
useNativeStateは、この状態をネイティブ側で扱えるフックです。WorkletCallbackと組み合わせると、ちらつきも抑えやすくなります。
useNativeStateは、iOSなら@expo/ui/swift-ui、Androidなら@expo/ui/jetpack-composeから読み込みます。
両方そろえるときは.ios.tsxと.android.tsxでファイルを分ける書き方がおすすめです。
TextInput
A text input backed by native SwiftUI and Jetpack Compose components, with a React Native-compatible API.
docs.expo.devimport { Host, Column, TextInput, Text } from "@expo/ui"
import { useNativeState } from "@expo/ui/swift-ui" // または /jetpack-compose
export default function SearchBar() {
const [query, setQuery] = useNativeState("")
return (
<Host style={{ flex: 1 }}>
<Column spacing={8}>
<TextInput value={query} onValueChange={setQuery} placeholder="検索..." />
<Text>入力中: {query}</Text>
</Column>
</Host>
)
}フォームバリデーション付きの長い入力フォームなど、入力レスポンスを少しでも良くしたい画面で効果を発揮します。
参考記事
- Expo SDK 56 Changelog: https://expo.dev/changelog/sdk-56
- Expo SDK 55 Changelog: https://expo.dev/changelog/sdk-55
- Expo UIドキュメント: https://docs.expo.dev/versions/latest/sdk/ui/
- Universal Components: https://docs.expo.dev/versions/v56.0.0/sdk/ui/universal/
- ExpoUI移行ガイド: https://docs.expo.dev/versions/v56.0.0/sdk/ui/drop-in-replacements/
さいごに
「SDK 56」のExpoUIは、ベータ版から安定版へ変わりこれから多くのプロジェクトで使われていくかと思います。
新規プロジェクトをcreate-expo-appで立ち上げると最初から@expo/uiが入ってくるので、「SDK 56」を機にまずは触ってみるのがおすすめです。
