旧ブログ(ISSEN)から移行しました

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

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

杉田侑祐
杉田侑祐10分で読めます
はてなブックマーク

はじめに

この記事の概要

こんにちは、株式会社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点が挙げられています。

  1. Universal Componentsの新登場
  2. ネイティブAPI(SwiftUI / Compose)の安定化
  3. コミュニティライブラリから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-domreact-native-webのどちらかを使う想定になります

提供されるのはレイアウト・テキスト・入力・コントロール・シート系の最小コンポーネントです。
レイアウト系にはHost / Row / Column / Spacer / ScrollViewがあります。
表示系にはText / Icon、入力系にはTextInput / Button / Switch / Slider / Checkboxが含まれます。
加えてBottomSheetFieldGroupといった構成系コンポーネントも提供されます。

これにより、これまで.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のフルカタログが使えます。

Material 3 Dynamic Colors と 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を使えば、入力イベントにぴったり寄せて処理できます。
結果として、従来の制御入力で出やすかった入力と表示のズレやちらつきを抑えやすくなります。

ネイティブ状態と同期 worklet コールバック useNativeState
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.dev
Hostをトップコンポーネントで使用する
import { 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.dev
FieldGroup
import { 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.dev
BottomSheet
import { 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.dev
TextInput
import { 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>
  )
}

フォームバリデーション付きの長い入力フォームなど、入力レスポンスを少しでも良くしたい画面で効果を発揮します。

参考記事

さいごに

「SDK 56」のExpoUIは、ベータ版から安定版へ変わりこれから多くのプロジェクトで使われていくかと思います。
新規プロジェクトをcreate-expo-appで立ち上げると最初から@expo/uiが入ってくるので、「SDK 56」を機にまずは触ってみるのがおすすめです。

この記事を書いた人

杉田侑祐
杉田侑祐

TOKOSのフロントエンドエンジニア兼UI/UXデザイナー。このブログではフロントエンドメインで投稿しています。HIPHOPとゲームが好きです✌️