mcpField
E
expo-dom

expo-dom

github.com/expo開発3.5k21.6k 回インストール今日更新
リポジトリ

expo-dom について

expo-dom は、Web の React コンポーネントをネイティブアプリの WebView 内で動かし、Web ではそのまま表示するための Expo スキルです。Web 専用ライブラリや既存の Web UI を Expo に取り込むチーム向けです。

主な機能

  1. 01ネイティブの WebView で Web React コードを実行
  2. 02Web 専用の React ライブラリを利用
  3. 03非同期の props でネイティブ操作を公開
  4. 04DOMProps による WebView 設定
  5. 05DOM コンポーネント内で Expo Router を利用

活用シーン

  1. 01Expo アプリにチャートや構文ハイライトを追加
  2. 02既存の React Web コンポーネントをネイティブで再利用
  3. 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 コンポーネントのルール

  1. ファイルの先頭に 'use dom'; ディレクティブを記述する必要があります
  2. デフォルトエクスポートは 1 つ — 1 ファイルにつき React コンポーネントは 1 つです
  3. 専用ファイルにする — インラインで定義したり、ネイティブコンポーネントと組み合わせたりすることはできません
  4. シリアライズ可能な props のみ — 文字列、数値、真偽値、配列、プレーンオブジェクト
  5. 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>
  );
}

プラットフォームごとの動作

プラットフォーム動作
iOSWKWebView 内でレンダリングされます
AndroidWebView 内でレンダリングされます
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 として指定してください。

コメント