mcpField
U
use-dom

use-dom

github.com/gigs-slc開発359 回インストール40 週間前に更新
リポジトリ

use-dom について

use-domは、ネイティブアプリでWebベースのReactコンポーネントを使うExpo開発者向けのスキルです。DOMコンポーネントはネイティブではWebViewで動作し、Webではそのまま描画されます。

主な機能

  1. 01Web専用Reactライブラリの利用
  2. 02ネイティブWebViewでのDOMコード実行
  3. 03シリアライズ可能なプロパティの受け渡し
  4. 04非同期でのネイティブ機能呼び出し
  5. 05Expo Router APIの利用

活用シーン

  1. 01Expoアプリへのチャートやコード強調表示の追加
  2. 02既存のReact Webコンポーネントの再利用
  3. 03ブラウザー環境が必要なコンテンツの埋め込み
  4. 04CanvasやWebGL体験の構築

インストール

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

$npx skills add https://github.com/gigs-slc/react-native-skills --skill use-dom

インストール後、Claude Code で/use-domを入力して読み込まれたか確認します。

詳細

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 コンテキストがあり、ネイティブと状態を直接共有できません

コメント