Add PostCSS configuration and skills lock file
- Created a new PostCSS configuration file to integrate Tailwind CSS. - Added a skills lock file containing various Expo skills with their respective source and computed hashes.
This commit is contained in:
417
.agents/skills/use-dom/SKILL.md
Normal file
417
.agents/skills/use-dom/SKILL.md
Normal file
@@ -0,0 +1,417 @@
|
||||
---
|
||||
name: use-dom
|
||||
description: Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally.
|
||||
version: 1.0.0
|
||||
license: MIT
|
||||
---
|
||||
|
||||
## What are DOM Components?
|
||||
|
||||
DOM components allow web code to run verbatim in a webview on native platforms while rendering as-is on web. This enables using web-only libraries like `recharts`, `react-syntax-highlighter`, or any React web library in your Expo app without modification.
|
||||
|
||||
## When to Use DOM Components
|
||||
|
||||
Use DOM components when you need:
|
||||
|
||||
- **Web-only libraries** — Charts (recharts, chart.js), syntax highlighters, rich text editors, or any library that depends on DOM APIs
|
||||
- **Migrating web code** — Bring existing React web components to native without rewriting
|
||||
- **Complex HTML/CSS layouts** — When CSS features aren't available in React Native
|
||||
- **iframes or embeds** — Embedding external content that requires a browser context
|
||||
- **Canvas or WebGL** — Web graphics APIs not available natively
|
||||
|
||||
## When NOT to Use DOM Components
|
||||
|
||||
Avoid DOM components when:
|
||||
|
||||
- **Native performance is critical** — Webviews add overhead
|
||||
- **Simple UI** — React Native components are more efficient for basic layouts
|
||||
- **Deep native integration** — Use local modules instead for native APIs
|
||||
- **Layout routes** — `_layout` files cannot be DOM components
|
||||
|
||||
## Basic DOM Component
|
||||
|
||||
Create a new file with the `'use dom';` directive at the top:
|
||||
|
||||
```tsx
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Rules for DOM Components
|
||||
|
||||
1. **Must have `'use dom';` directive** at the top of the file
|
||||
2. **Single default export** — One React component per file
|
||||
3. **Own file** — Cannot be defined inline or combined with native components
|
||||
4. **Serializable props only** — Strings, numbers, booleans, arrays, plain objects
|
||||
5. **Include CSS in the component file** — DOM components run in isolated context
|
||||
|
||||
## The `dom` Prop
|
||||
|
||||
Every DOM component receives a special `dom` prop for webview configuration. Always type it in your props:
|
||||
|
||||
```tsx
|
||||
"use dom";
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
dom: import("expo/dom").DOMProps;
|
||||
}
|
||||
|
||||
export default function MyComponent({ content }: Props) {
|
||||
return <div>{content}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Common `dom` Prop Options
|
||||
|
||||
```tsx
|
||||
// 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 }
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Exposing Native Actions to the Webview
|
||||
|
||||
Pass async functions as props to expose native functionality to the DOM component:
|
||||
|
||||
```tsx
|
||||
// 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// 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>;
|
||||
}
|
||||
```
|
||||
|
||||
## Using Web Libraries
|
||||
|
||||
DOM components can use any web library:
|
||||
|
||||
```tsx
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## CSS in DOM Components
|
||||
|
||||
CSS imports must be in the DOM component file since they run in isolated context:
|
||||
|
||||
```tsx
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Or use inline styles / CSS-in-JS:
|
||||
|
||||
```tsx
|
||||
"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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Expo Router in DOM Components
|
||||
|
||||
The expo-router `<Link />` component and router API work inside DOM components:
|
||||
|
||||
```tsx
|
||||
"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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Router APIs That Require Props
|
||||
|
||||
These hooks don't work directly in DOM components because they need synchronous access to native routing state:
|
||||
|
||||
- `useLocalSearchParams()`
|
||||
- `useGlobalSearchParams()`
|
||||
- `usePathname()`
|
||||
- `useSegments()`
|
||||
- `useRootNavigation()`
|
||||
- `useRootNavigationState()`
|
||||
|
||||
**Solution:** Read these values in the native parent and pass as props:
|
||||
|
||||
```tsx
|
||||
// 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} />;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Detecting DOM Environment
|
||||
|
||||
Check if code is running in a DOM component:
|
||||
|
||||
```tsx
|
||||
"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>;
|
||||
}
|
||||
```
|
||||
|
||||
## Assets
|
||||
|
||||
Prefer requiring assets instead of using the public directory:
|
||||
|
||||
```tsx
|
||||
"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" />;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage from Native Components
|
||||
|
||||
Import and use DOM components like regular components:
|
||||
|
||||
```tsx
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Platform Behavior
|
||||
|
||||
| Platform | Behavior |
|
||||
| -------- | ----------------------------------- |
|
||||
| iOS | Rendered in WKWebView |
|
||||
| Android | Rendered in WebView |
|
||||
| Web | Rendered as-is (no webview wrapper) |
|
||||
|
||||
On web, the `dom` prop is ignored since no webview is needed.
|
||||
|
||||
## Tips
|
||||
|
||||
- DOM components hot reload during development
|
||||
- Keep DOM components focused — don't put entire screens in webviews
|
||||
- Use native components for navigation chrome, DOM components for specialized content
|
||||
- Test on all platforms — web rendering may differ slightly from native webviews
|
||||
- Large DOM components may impact performance — profile if needed
|
||||
- The webview has its own JavaScript context — cannot directly share state with native
|
||||
Reference in New Issue
Block a user