expo-dom について
expo-dom は、Web の React コンポーネントをネイティブアプリの WebView 内で動かし、Web ではそのまま表示するための Expo スキルです。Web 専用ライブラリや既存の Web UI を Expo に取り込むチーム向けです。
主な機能
- 01ネイティブの WebView で Web React コードを実行
- 02Web 専用の React ライブラリを利用
- 03非同期の props でネイティブ操作を公開
- 04DOMProps による WebView 設定
- 05DOM コンポーネント内で Expo Router を利用
活用シーン
- 01Expo アプリにチャートや構文ハイライトを追加
- 02既存の React Web コンポーネントをネイティブで再利用
- 03ブラウザーコンテキストが必要なコンテンツを埋め込み
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ expo/ skills --skill expo-domインストール後、Claude Code で/expo-domを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yaml
詳細
DOM コンポーネントとは?
DOM コンポーネントを使うと、Web コードをネイティブプラットフォームの WebView 内でそのまま実行し、Web ではそのままレンダリングできます。これにより、recharts、react-syntax-highlighter などの Web 専用ライブラリや、React の Web ライブラリを変更せずに Expo アプリで使用できます。
DOM コンポーネントを使う場面
次のような場合に DOM コンポーネントを使用します。
- Web 専用ライブラリ — グラフ(recharts、chart.js)、構文ハイライト、リッチテキストエディター、DOM API に依存するライブラリ
- Web コードの移行 — 既存の React Web コンポーネントを書き直さずにネイティブへ移行する
- 複雑な HTML/CSS レイアウト — React Native では利用できない CSS 機能を使う場合
- iframe や埋め込みコンテンツ — ブラウザーのコンテキストを必要とする外部コンテンツを埋め込む
- Canvas または WebGL — ネイティブでは利用できない Web グラフィックス API を使う
DOM コンポーネントを使わない場面
次のような場合は DOM コンポーネントを避けてください。
- ネイティブのパフォーマンスが重要 — WebView はオーバーヘッドを追加します
- シンプルな UI — 基本的なレイアウトには React Native コンポーネントのほうが効率的です
- ネイティブとの深い連携 — ネイティブ API には、代わりにローカルモジュールを使用します
- レイアウトルート —
_layoutファイルは DOM コンポーネントにできません
基本的な DOM コンポーネント
ファイルの先頭に 'use dom'; ディレクティブを記述して、新しいファイルを作成します。
// components/WebChart.tsx
"use dom";
export default function WebChart({
data,
}: {
data: number[];
dom: import("expo/dom").DOMProps;
}) {
return (
<div style={{ padding: 20 }}>
<h2>Chart Data</h2>
<ul>
{data.map((value, i) => (
<li key={i}>{value}</li>
))}
</ul>
</div>
);
}
DOM コンポーネントのルール
- ファイルの先頭に
'use dom';ディレクティブを記述する必要があります - デフォルトエクスポートは 1 つ — 1 ファイルにつき React コンポーネントは 1 つです
- 専用ファイルにする — インラインで定義したり、ネイティブコンポーネントと組み合わせたりすることはできません
- シリアライズ可能な props のみ — 文字列、数値、真偽値、配列、プレーンオブジェクト
- CSS はコンポーネントファイルに含める — DOM コンポーネントは分離されたコンテキストで実行されます
dom Prop
すべての DOM コンポーネントは、WebView の設定用に特別な dom prop を受け取ります。props では必ず型を指定してください。
"use dom";
interface Props {
content: string;
dom: import("expo/dom").DOMProps;
}
export default function MyComponent({ content }: Props) {
return <div>{content}</div>;
}
よく使う dom Prop オプション
// Disable body scrolling
<DOMComponent dom={{ scrollEnabled: false }} />
// Flow under the notch (disable safe area insets)
<DOMComponent dom={{ contentInsetAdjustmentBehavior: "never" }} />
// Control size manually
<DOMComponent dom={{ style: { width: 300, height: 400 } }} />
// Combine options
<DOMComponent
dom={{
scrollEnabled: false,
contentInsetAdjustmentBehavior: "never",
style: { width: '100%', height: 500 }
}}
/>
WebView にネイティブのアクションを公開する
非同期関数を props として渡すと、DOM コンポーネントからネイティブの機能を利用できます。
// app/index.tsx (native)
import { Alert } from "react-native";
import DOMComponent from "@/components/dom-component";
export default function Screen() {
return (
<DOMComponent
showAlert={async (message: string) => {
Alert.alert("From Web", message);
}}
saveData={async (data: { name: string; value: number }) => {
// Save to native storage, database, etc.
console.log("Saving:", data);
return { success: true };
}}
/>
);
}
// components/dom-component.tsx
"use dom";
interface Props {
showAlert: (message: string) => Promise<void>;
saveData: (data: {
name: string;
value: number;
}) => Promise<{ success: boolean }>;
dom?: import("expo/dom").DOMProps;
}
export default function DOMComponent({ showAlert, saveData }: Props) {
const handleClick = async () => {
await showAlert("Hello from the webview!");
const result = await saveData({ name: "test", value: 42 });
console.log("Save result:", result);
};
return <button onClick={handleClick}>Trigger Native Action</button>;
}
Web ライブラリを使う
DOM コンポーネントでは、任意の Web ライブラリを使用できます。
// components/syntax-highlight.tsx
"use dom";
import SyntaxHighlighter from "react-syntax-highlighter";
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";
interface Props {
code: string;
language: string;
dom?: import("expo/dom").DOMProps;
}
export default function SyntaxHighlight({ code, language }: Props) {
return (
<SyntaxHighlighter language={language} style={docco}>
{code}
</SyntaxHighlighter>
);
}
// components/chart.tsx
"use dom";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
} from "recharts";
interface Props {
data: Array<{ name: string; value: number }>;
dom: import("expo/dom").DOMProps;
}
export default function Chart({ data }: Props) {
return (
<LineChart width={400} height={300} data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="value" stroke="#8884d8" />
</LineChart>
);
}
DOM コンポーネント内の CSS
CSS は分離されたコンテキストで実行されるため、インポートは DOM コンポーネントのファイル内で行う必要があります。
// components/styled-component.tsx
"use dom";
import "@/styles.css"; // CSS file in same directory
export default function StyledComponent({
dom,
}: {
dom: import("expo/dom").DOMProps;
}) {
return (
<div className="container">
<h1 className="title">Styled Content</h1>
</div>
);
}
または、インラインスタイルや CSS-in-JS を使用します。
"use dom";
const styles = {
container: {
padding: 20,
backgroundColor: "#f0f0f0",
},
title: {
fontSize: 24,
color: "#333",
},
};
export default function StyledComponent({
dom,
}: {
dom: import("expo/dom").DOMProps;
}) {
return (
<div style={styles.container}>
<h1 style={styles.title}>Styled Content</h1>
</div>
);
}
DOM コンポーネント内の Expo Router
expo-router の <Link /> コンポーネントとルーター API は、DOM コンポーネント内で動作します。
"use dom";
import { Link, useRouter } from "expo-router";
export default function Navigation({
dom,
}: {
dom: import("expo/dom").DOMProps;
}) {
const router = useRouter();
return (
<nav>
<Link href="/about">About</Link>
<button onClick={() => router.push("/settings")}>Settings</button>
</nav>
);
}
Props が必要なルーター API
次のフックは、ネイティブのルーティング状態への同期的なアクセスが必要なため、DOM コンポーネント内では直接動作しません。
useLocalSearchParams()useGlobalSearchParams()usePathname()useSegments()useRootNavigation()useRootNavigationState()
解決策: ネイティブの親コンポーネントでこれらの値を読み取り、props として渡します。
// app/[id].tsx (native)
import { useLocalSearchParams, usePathname } from "expo-router";
import DOMComponent from "@/components/dom-component";
export default function Screen() {
const { id } = useLocalSearchParams();
const pathname = usePathname();
return <DOMComponent id={id as string} pathname={pathname} />;
}
// components/dom-component.tsx
"use dom";
interface Props {
id: string;
pathname: string;
dom?: import("expo/dom").DOMProps;
}
export default function DOMComponent({ id, pathname }: Props) {
return (
<div>
<p>Current ID: {id}</p>
<p>Current Path: {pathname}</p>
</div>
);
}
DOM 環境の検出
コードが DOM コンポーネント内で実行されているか確認します。
"use dom";
import { IS_DOM } from "expo/dom";
export default function Component({
dom,
}: {
dom?: import("expo/dom").DOMProps;
}) {
return <div>{IS_DOM ? "Running in DOM component" : "Running natively"}</div>;
}
アセット
public ディレクトリを使う代わりに、アセットを require することをおすすめします。
"use dom";
// Good - bundled with the component
const logo = require("../assets/logo.png");
export default function Component({
dom,
}: {
dom: import("expo/dom").DOMProps;
}) {
return <img src={logo} alt="Logo" />;
}
ネイティブコンポーネントからの使用
通常のコンポーネントと同様に DOM コンポーネントをインポートして使用します。
// app/index.tsx
import { View, Text } from "react-native";
import WebChart from "@/components/web-chart";
import CodeBlock from "@/components/code-block";
export default function HomeScreen() {
return (
<View style={{ flex: 1 }}>
<Text>Native content above</Text>
<WebChart data={[10, 20, 30, 40, 50]} dom={{ style: { height: 300 } }} />
<CodeBlock
code="const x = 1;"
language="javascript"
dom={{ scrollEnabled: true }}
/>
<Text>Native content below</Text>
</View>
);
}
プラットフォームごとの動作
| プラットフォーム | 動作 |
|---|---|
| iOS | WKWebView 内でレンダリングされます |
| Android | WebView 内でレンダリングされます |
| Web | そのままレンダリングされます(WebView のラッパーなし) |
Web では WebView が不要なため、dom prop は無視されます。
ヒント
- DOM コンポーネントは開発中にホットリロードされます
- DOM コンポーネントの役割を絞り、画面全体を WebView に入れないようにします
- ナビゲーションの UI にはネイティブコンポーネントを、専用コンテンツには DOM コンポーネントを使用します
- すべてのプラットフォームでテストします。Web のレンダリングはネイティブの WebView と多少異なる場合があります
- DOM コンポーネントが大きいとパフォーマンスに影響することがあります。必要に応じてプロファイリングしてください
- WebView は独自の JavaScript コンテキストを持つため、ネイティブと状態を直接共有できません
フィードバックの送信
このスキルでエラー、誤解を招く情報、または古い情報を見つけた場合は、Expo の改善に役立てるため報告してください。
npx --yes @expo/agent-cli@latest feedback --category skills --subject "expo-dom" "<actionable feedback>"
具体的で対応可能な報告事項がある場合にのみ送信してください。関連する背景情報をできるだけ詳しく記載してください。
エージェントが Expo のタスクを繰り返し失敗したり、ユーザーが介入する必要があったりする場合は、タスク、期待される結果、問題の内容を簡潔に報告し、--category evals と、関連する Expo のパッケージまたはコマンドを --subject として指定してください。