Skip to content

Commit b4af14e

Browse files
committed
doc(readme): update
1 parent 3bc7ed6 commit b4af14e

1 file changed

Lines changed: 231 additions & 72 deletions

File tree

README.md

Lines changed: 231 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,296 @@
11
# @fictjs/react
22

3-
A React interoperability layer for Fict, based on a controlled React Islands model.
3+
React interoperability layer for [Fict](https://github.com/nicepkg/fict) — embed React components inside Fict applications as controlled islands with SSR, lazy loading, and fine-grained prop reactivity.
4+
5+
## Why
6+
7+
Fict uses its own compiler-driven reactivity model. When you need to reuse an existing React component (a design system, a charting library, a rich text editor), `@fictjs/react` bridges the gap: the React subtree runs in its own React root while the surrounding Fict app feeds it reactive props.
48

59
## Features
610

7-
- `reactify`: Wrap a React component as a Fict component (CSR + SSR).
8-
- `ReactIsland`: Declarative island component with `props` getter support.
9-
- `reactify$`: QRL-serializable React island with lazy loading support.
10-
- `installReactIslands`: Scan and mount `data-fict-react` islands on the client.
11-
- `reactAction$`: Pass Fict QRL actions as serializable React callbacks.
12-
- `fictReactPreset`: Isolate React JSX transform by directory in Vite (default `src/react/**`).
11+
| Capability | API | When to use |
12+
|---|---|---|
13+
| Eager wrapping | `reactify` | The React component is already imported |
14+
| Declarative island | `ReactIsland` | Inline island with a `props` getter |
15+
| Resumable / lazy | `reactify$` | The component should be lazy-loaded via QRL |
16+
| Static loader | `installReactIslands` | Mount islands from plain HTML attributes (no Fict runtime) |
17+
| Serializable callbacks | `reactAction$` | Pass Fict actions across the serialization boundary |
18+
| Vite preset | `fictReactPreset` | Isolate React JSX transform from Fict's compiler |
1319

1420
## Install
1521

1622
```bash
1723
pnpm add @fictjs/react react react-dom @fictjs/runtime
1824
```
1925

20-
## Usage
26+
For the Vite preset (optional):
27+
28+
```bash
29+
pnpm add -D @vitejs/plugin-react vite
30+
```
31+
32+
### Requirements
2133

22-
### 1) `reactify` (Eager)
34+
- Node 20+
35+
- React 18.2+ or 19
36+
- `@fictjs/runtime` >= 0.10.0
37+
38+
## Quick Start
39+
40+
### 1. Vite Configuration
41+
42+
If your project mixes Fict and React files, use the preset to scope the React JSX transform to a specific directory (default: `src/react/**`):
2343

2444
```ts
45+
// vite.config.ts
46+
import { defineConfig } from 'vite'
47+
import { fictReactPreset } from '@fictjs/react/preset'
48+
import fict from '@fictjs/vite-plugin'
49+
50+
export default defineConfig({
51+
plugins: [
52+
fict(),
53+
...fictReactPreset(),
54+
],
55+
})
56+
```
57+
58+
Custom scope:
59+
60+
```ts
61+
fictReactPreset({
62+
include: [/components\/react\/.*\.[jt]sx?$/],
63+
})
64+
```
65+
66+
### 2. Wrap a React Component (Eager)
67+
68+
```tsx
2569
import { reactify } from '@fictjs/react'
2670
import { prop } from '@fictjs/runtime'
71+
import { MyButton } from './react/MyButton'
2772

28-
function ReactButton(props: { text: string }) {
29-
return <button>{props.text}</button>
30-
}
31-
32-
const FictButton = reactify(ReactButton)
73+
const FictButton = reactify(MyButton)
3374

34-
// Use in Fict
35-
;<FictButton text={prop(() => state.text)} />
75+
// In a Fict component
76+
function App() {
77+
let count = $state(0)
78+
return <FictButton label={prop(() => `Clicked ${count} times`)} />
79+
}
3680
```
3781

38-
### 2) `ReactIsland`
82+
The React component re-renders whenever the reactive props change — without re-running the Fict component function.
3983

40-
```ts
41-
import { ReactIsland } from '@fictjs/react'
84+
### 3. Declarative Island
4285

43-
<ReactIsland
44-
component={ReactButton}
45-
props={() => ({ text: state.text })}
46-
client="visible"
47-
ssr
48-
/>
86+
```tsx
87+
import { ReactIsland } from '@fictjs/react'
88+
import { Chart } from './react/Chart'
89+
90+
function Dashboard() {
91+
let data = $state([])
92+
93+
return (
94+
<ReactIsland
95+
component={Chart}
96+
props={() => ({ data, height: 300 })}
97+
client="visible"
98+
ssr
99+
/>
100+
)
101+
}
49102
```
50103

51-
### 3) `reactify$` (Resumable)
104+
### 4. Lazy-Loaded Island (Resumable)
52105

53106
```ts
54107
import { reactify$ } from '@fictjs/react'
55108

56-
export const FictButton$ = reactify$({
109+
export const LazyChart = reactify$({
57110
module: import.meta.url,
58-
export: 'ReactButton',
111+
export: 'Chart',
59112
client: 'idle',
60113
ssr: true,
61114
})
62115
```
63116

64-
> `reactify$` outputs `data-fict-react` + `data-fict-react-props`, which can be mounted on the client by strategy.
117+
The component module is loaded only when the client strategy fires. On the server the optional `component` reference is used for SSR; on the client the QRL triggers a dynamic import.
118+
119+
Serialized props are written to `data-fict-react-props` on the host element, making the island fully resumable from server-rendered HTML.
120+
121+
### 5. Static Islands (Loader)
65122

66-
### 4) Client Loader
123+
Mount React components from plain HTML without any Fict runtime involvement:
124+
125+
```html
126+
<div
127+
data-fict-react="./components/Widget.js#Widget"
128+
data-fict-react-client="visible"
129+
data-fict-react-props="%7B%22title%22%3A%22Hello%22%7D"
130+
></div>
131+
```
67132

68133
```ts
69-
import { installReactIslands } from '@fictjs/react'
134+
import { installReactIslands } from '@fictjs/react/loader'
70135

71-
installReactIslands()
136+
const cleanup = installReactIslands({
137+
observe: true, // Watch for dynamically added islands
138+
defaultClient: 'idle', // Fallback client strategy
139+
visibleRootMargin: '200px',
140+
})
141+
142+
// Later: cleanup() to disconnect observer and unmount all islands
72143
```
73144

74-
`installReactIslands` host attribute constraints:
145+
The loader uses `MutationObserver` to detect new island hosts and attribute changes. Updating `data-fict-react-props` on a mounted host triggers a React re-render. Changing the QRL (`data-fict-react`) disposes the old root and mounts a fresh one.
75146

76-
- Dynamically updatable (triggers refresh): `data-fict-react-props`, `data-fict-react-action-props`
77-
- Immutable after initialization (recreate the island host to apply changes):
78-
`data-fict-react-client`, `data-fict-react-ssr`, `data-fict-react-prefix`
79-
- Mutable and triggers runtime rebuild: `data-fict-react` (QRL changes cause dispose + remount)
80-
- Runtime mutations of immutable attributes: warning in development, silent ignore in production
147+
### 6. Serializable Actions
81148

82-
### 5) Serializable Callback (Action)
149+
Pass callbacks from Fict to React across the serialization boundary:
83150

84151
```ts
85152
import { reactAction$ } from '@fictjs/react'
86153

87-
<RemoteReactIsland
88-
onAction={reactAction$(import.meta.url, 'handleAction')}
154+
// In a Fict component
155+
<RemoteEditor
156+
onSave={reactAction$(import.meta.url, 'handleSave')}
89157
/>
90158
```
91159

92-
By default, action refs in callback-like props (`/^on[A-Z]/`) are materialized into callable functions.
93-
If your callback prop is not named like `onX`, declare it explicitly via `actionProps`:
160+
```ts
161+
// Same module — the exported handler
162+
export function handleSave(content: string) {
163+
console.log('Saved:', content)
164+
}
165+
```
166+
167+
Props matching `/^on[A-Z]/` are automatically detected as action refs. For non-standard callback prop names, declare them explicitly:
94168

95169
```ts
96-
const RemoteReactIsland = reactify$({
170+
const RemoteEditor = reactify$({
97171
module: import.meta.url,
98-
export: 'RemoteReactIsland',
99-
actionProps: ['submitAction'],
172+
export: 'Editor',
173+
actionProps: ['submitHandler', 'validateFn'],
100174
})
101175
```
102176

103-
For loader-based usage, pass this through host attributes:
177+
## Client Strategies
104178

105-
```html
106-
<div
107-
data-fict-react="...#RemoteReactIsland"
108-
data-fict-react-action-props="%5B%22submitAction%22%5D"
109-
></div>
110-
```
179+
Control when each island mounts on the client:
111180

112-
### 6) Vite Preset (React Lane)
181+
| Strategy | Behavior |
182+
|---|---|
183+
| `'load'` | Mount immediately (via microtask). **Default.** |
184+
| `'idle'` | Mount during idle time (`requestIdleCallback`, falls back to `setTimeout(…, 1)`) |
185+
| `'visible'` | Mount when the host element enters the viewport (`IntersectionObserver` with configurable `rootMargin`, default `200px`) |
186+
| `'only'` | Client-only rendering — no SSR, no hydration |
113187

114-
```ts
115-
import { defineConfig } from 'vite'
116-
import { fictReactPreset } from '@fictjs/react/preset'
188+
When `ssr` is `true` (the default), the React subtree is rendered to HTML on the server. On the client, the island hydrates (`hydrateRoot`) if SSR content is present, otherwise it creates a fresh root (`createRoot`).
117189

118-
export default defineConfig({
119-
plugins: [
120-
...fictReactPreset({
121-
include: [/src\/react\/.*\.[jt]sx?$/],
122-
}),
123-
],
124-
})
190+
## API Reference
191+
192+
### `reactify<P>(component, options?)`
193+
194+
Wraps a React component as a Fict component. Props flow reactively from the Fict side; the React root updates when props change.
195+
196+
**Options** (`ReactInteropOptions`):
197+
198+
| Option | Type | Default | Description |
199+
|---|---|---|---|
200+
| `ssr` | `boolean` | `true` | Server-side render the React subtree |
201+
| `client` | `ClientDirective` | `'load'` | Client mount strategy |
202+
| `visibleRootMargin` | `string` | `'200px'` | Margin for `'visible'` strategy |
203+
| `identifierPrefix` | `string` | `''` | React `useId` prefix for multi-root pages |
204+
| `actionProps` | `string[]` | `[]` | Additional callback prop names to materialize |
205+
206+
### `ReactIsland<P>(props)`
207+
208+
Declarative island component. Accepts `component`, `props` (value or getter), and all `ReactInteropOptions`.
209+
210+
### `reactify$<P>(options)`
211+
212+
Creates a lazy-loadable Fict component backed by a QRL.
213+
214+
**Additional options** (`ReactifyQrlOptions<P>`):
215+
216+
| Option | Type | Description |
217+
|---|---|---|
218+
| `module` | `string` | Module URL, usually `import.meta.url` |
219+
| `export` | `string` | Export name (default: `'default'`) |
220+
| `component` | `ComponentType<P>` | Optional eager reference for SSR |
221+
222+
### `installReactIslands(options?)`
223+
224+
Scans the document for `[data-fict-react]` hosts and mounts them. Returns a cleanup function.
225+
226+
**Options** (`ReactIslandsLoaderOptions`):
227+
228+
| Option | Type | Default | Description |
229+
|---|---|---|---|
230+
| `document` | `Document` | `document` | Document to scan |
231+
| `selector` | `string` | `'[data-fict-react]'` | CSS selector for island hosts |
232+
| `observe` | `boolean` | `true` | Watch for dynamic additions/removals |
233+
| `defaultClient` | `ClientDirective` | `'load'` | Fallback client strategy |
234+
| `visibleRootMargin` | `string` | `'200px'` | Margin for `'visible'` strategy |
235+
236+
### `reactAction$(moduleId, exportName?)`
237+
238+
Creates a serializable action ref from a module export. The ref is materialized into a callable function when the React component mounts.
239+
240+
### `reactActionFromQrl(qrl)`
241+
242+
Creates an action ref from a raw QRL string.
243+
244+
### `fictReactPreset(options?)`
245+
246+
Returns Vite plugins that scope the React JSX transform to a directory.
247+
248+
| Option | Type | Default | Description |
249+
|---|---|---|---|
250+
| `include` | `FilterPattern` | `[/src\/react\/.*\.[jt]sx?$/]` | Files to transform with React JSX |
251+
| `exclude` | `FilterPattern` || Files to exclude |
252+
| `react` | `ReactPluginOptions` || Additional `@vitejs/plugin-react` options |
253+
254+
## Host Attributes
255+
256+
When using the loader or resumable mode, the following data attributes control island behavior:
257+
258+
| Attribute | Mutable | Purpose |
259+
|---|---|---|
260+
| `data-fict-react` | `*` | QRL pointing to the React component module |
261+
| `data-fict-react-props` | yes | URL-encoded serialized props |
262+
| `data-fict-react-action-props` | yes | URL-encoded JSON array of custom action prop names |
263+
| `data-fict-react-client` | no | Client strategy (`load` / `idle` / `visible` / `only`) |
264+
| `data-fict-react-ssr` | no | `'1'` if SSR content is present |
265+
| `data-fict-react-prefix` | no | React `useId` identifier prefix |
266+
| `data-fict-react-host` || Marks element as a React island host |
267+
| `data-fict-react-mounted` || Set to `'1'` after the island mounts |
268+
269+
`*` Changing the QRL disposes the current root and creates a new one.
270+
271+
Immutable attributes (`data-fict-react-client`, `data-fict-react-ssr`, `data-fict-react-prefix`) emit a warning in development if mutated at runtime. To change them, recreate the host element.
272+
273+
## Package Exports
274+
275+
```
276+
@fictjs/react → Main API (reactify, ReactIsland, reactify$, reactAction$, …)
277+
@fictjs/react/loader → installReactIslands
278+
@fictjs/react/preset → fictReactPreset
125279
```
126280

127-
## Client Strategies
281+
## Development
128282

129-
- `load`: Mount as soon as possible.
130-
- `idle`: Mount when idle (`requestIdleCallback` preferred).
131-
- `visible`: Mount when entering the viewport (`IntersectionObserver`).
132-
- `only`: Client-only rendering (no SSR hydrate).
283+
```bash
284+
pnpm install
285+
pnpm dev # Watch mode
286+
pnpm build # Production build
287+
pnpm test # Unit tests (vitest)
288+
pnpm test:it # Integration tests
289+
pnpm test:e2e # E2E tests (Playwright + Chromium)
290+
pnpm lint # ESLint
291+
pnpm typecheck # TypeScript validation
292+
```
133293

134-
## Internal Hooks
294+
## License
135295

136-
- `src/testing.ts` provides test injection hooks for this repository only.
137-
- These hooks are internal/unstable, not part of package exports, and not covered by compatibility guarantees.
296+
[MIT](./LICENSE)

0 commit comments

Comments
 (0)