mcpField
U
use-dom

use-dom

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

use-dom について

use-dom は、Expo DOM Components の開発方法を案内するスキルです。Web コードをネイティブでは WebView で実行し、Web では直接描画することで、React Native 開発者が既存の React コンポーネントや Web ライブラリを再利用できます。

主な機能

  1. 01ネイティブの WebView で Web コードを実行
  2. 02Web では直接描画
  3. 03Web 専用の React ライブラリを利用
  4. 04シリアライズ可能な props を渡す
  5. 05非同期 props でネイティブ操作を公開

活用シーン

  1. 01Web ライブラリによるチャートの追加
  2. 02既存の React Web コンポーネントの再利用
  3. 03ブラウザー環境が必要なコンテンツの埋め込み
  4. 04複雑な HTML と CSS のレイアウト描画

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/expo/skills --skill use-dom

インストール後、Claude Code で/use-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 レイアウト — CSS の機能が React Native で利用できない場合
  • 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 に入れないようにしてください
  • ナビゲーションの枠組みにはネイティブコンポーネントを、特殊なコンテンツには DOM コンポーネントを使用してください
  • すべてのプラットフォームでテストしてください。Web での描画はネイティブの WebView と多少異なる場合があります
  • DOM コンポーネントが大きいとパフォーマンスに影響することがあります。必要に応じてプロファイリングしてください
  • WebView には独自の JavaScript コンテキストがあり、ネイティブと状態を直接共有できません

コメント