@void/react 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -215
- package/dist/plugin.mjs +7 -2
- package/dist/runtime/pages-client.mjs +4 -3
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# @void/react
|
|
2
2
|
|
|
3
|
-
React
|
|
3
|
+
Build server-rendered Void pages with React, server loaders, and typed forms. Requires React 19.
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Install alongside `void`:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @void/react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Add the adapter to your Vite config. It includes the framework's Vite plugin:
|
|
8
14
|
|
|
9
15
|
```ts
|
|
10
16
|
import { defineConfig } from 'vite';
|
|
@@ -16,217 +22,6 @@ export default defineConfig({
|
|
|
16
22
|
});
|
|
17
23
|
```
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`voidReact(options?)` returns:
|
|
22
|
-
|
|
23
|
-
- `@vitejs/plugin-react`
|
|
24
|
-
- the React pages plugin
|
|
25
|
-
- the React islands plugin
|
|
26
|
-
|
|
27
|
-
Options:
|
|
28
|
-
|
|
29
|
-
- `react?` — forwarded to `@vitejs/plugin-react`
|
|
30
|
-
- `viewTransitions?` — enable View Transitions for SPA navigation
|
|
31
|
-
- `prefetch?` — link prefetch behavior
|
|
32
|
-
|
|
33
|
-
## Runtime
|
|
34
|
-
|
|
35
|
-
Main runtime APIs live at `@void/react`:
|
|
36
|
-
|
|
37
|
-
- `Link`
|
|
38
|
-
- `useRouter()`
|
|
39
|
-
- `useParams()`
|
|
40
|
-
- `useNavigation()`
|
|
41
|
-
- `useShared()`
|
|
42
|
-
- `Form` and `useForm()`
|
|
43
|
-
- `useIslandForm()`
|
|
44
|
-
- `action()`
|
|
45
|
-
|
|
46
|
-
### Async React Routing
|
|
47
|
-
|
|
48
|
-
React Pages navigations are scheduled as React transitions. Route data,
|
|
49
|
-
component modules, layouts, and deferred props are prepared as resources, then
|
|
50
|
-
revealed through Suspense so the current screen can stay visible while the next
|
|
51
|
-
page loads.
|
|
52
|
-
|
|
53
|
-
```tsx
|
|
54
|
-
import { Suspense, use } from 'react';
|
|
55
|
-
import { useNavigation, useRouter } from '@void/react';
|
|
56
|
-
import type { Props } from './dashboard.server';
|
|
57
|
-
|
|
58
|
-
function Usage({ usage }: Pick<Props, 'usage'>) {
|
|
59
|
-
const resolved = use(usage);
|
|
60
|
-
return <p>{resolved.requests} requests</p>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export default function Dashboard({ usage }: Props) {
|
|
64
|
-
const router = useRouter();
|
|
65
|
-
const navigation = useNavigation();
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<>
|
|
69
|
-
<button onClick={() => router.refresh()} disabled={navigation.state !== 'idle'}>
|
|
70
|
-
Refresh
|
|
71
|
-
</button>
|
|
72
|
-
<Suspense fallback={<p>Loading usage...</p>}>
|
|
73
|
-
<Usage usage={usage} />
|
|
74
|
-
</Suspense>
|
|
75
|
-
</>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Breaking change: React deferred props returned from `defer()` are consumed as
|
|
81
|
-
promises with `use()` under a Suspense boundary. The old
|
|
82
|
-
`prop.loading` / `prop.value` shape remains the runtime shape for the Vue,
|
|
83
|
-
Svelte, and Solid adapters.
|
|
84
|
-
|
|
85
|
-
SSR uses React 19's streaming renderer. Deferred props render the nearest
|
|
86
|
-
Suspense fallback in the initial shell, then Void streams the resolved values to
|
|
87
|
-
the hydrated client. Put deferred reads behind Suspense, and use a normal React
|
|
88
|
-
error boundary around that Suspense boundary when rejected deferred values need a
|
|
89
|
-
custom error state.
|
|
90
|
-
|
|
91
|
-
Export `ssr = false` from a page's `.server.ts` file to skip server-rendered
|
|
92
|
-
component HTML for that route. The loader still runs and the browser mounts the
|
|
93
|
-
page with the embedded initial props.
|
|
94
|
-
|
|
95
|
-
For explicit prop annotations in React pages, import `Deferred` from
|
|
96
|
-
`@void/react`:
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
import type { Deferred } from '@void/react';
|
|
100
|
-
|
|
101
|
-
export interface Props {
|
|
102
|
-
usage: Deferred<{ requests: number }>;
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Forms and Actions
|
|
107
|
-
|
|
108
|
-
`useForm()` is built on React Actions. Prefer native form submission with
|
|
109
|
-
`form.post`, `form.put`, `form.patch`, or `form.delete`; use `useFormStatus()`
|
|
110
|
-
inside the form for button pending state.
|
|
111
|
-
|
|
112
|
-
```tsx
|
|
113
|
-
import { Form, useForm } from '@void/react';
|
|
114
|
-
import { useFormStatus } from 'react-dom';
|
|
115
|
-
|
|
116
|
-
function SubmitButton() {
|
|
117
|
-
const { pending } = useFormStatus();
|
|
118
|
-
return <button disabled={pending}>Save</button>;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export default function CreateUser() {
|
|
122
|
-
const form = useForm('/users', { name: '', email: '' });
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<Form form={form}>
|
|
126
|
-
<input
|
|
127
|
-
name="name"
|
|
128
|
-
value={form.data.name}
|
|
129
|
-
onChange={(e) => form.setData('name', e.target.value)}
|
|
130
|
-
/>
|
|
131
|
-
{form.errors.name && <span>{form.errors.name}</span>}
|
|
132
|
-
<SubmitButton />
|
|
133
|
-
</Form>
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
`form.post`, `form.put`, `form.patch`, and `form.delete` are native React form
|
|
139
|
-
actions. Use `action()` for the awaitable imperative escape hatch; it returns
|
|
140
|
-
`{ ok: true, pageData }` or `{ ok: false, error }` for call-site action errors,
|
|
141
|
-
while boundary-class failures are thrown. `action()` uses `POST` by default and
|
|
142
|
-
accepts `{ data, method, params }`.
|
|
143
|
-
|
|
144
|
-
### `useRouter`
|
|
145
|
-
|
|
146
|
-
`useRouter()` returns the Void Router. `visit()`, `refresh()`, and `prefetch()`
|
|
147
|
-
are awaitable; `prefetch()` resolves when the page data has been cached.
|
|
148
|
-
|
|
149
|
-
### `useParams`
|
|
150
|
-
|
|
151
|
-
`useParams()` returns dynamic route params for the page currently being rendered.
|
|
152
|
-
Use it instead of parsing `useRouter().path` in dynamic Pages routes.
|
|
153
|
-
|
|
154
|
-
### `Link`
|
|
155
|
-
|
|
156
|
-
`Link` is the React Pages navigation component. GET links render an `<a>` and
|
|
157
|
-
client-side navigate when the click is safe to intercept. Mutation links render a
|
|
158
|
-
`<button type="button">` and submit through the Void Router.
|
|
159
|
-
|
|
160
|
-
```tsx
|
|
161
|
-
import { Link } from '@void/react';
|
|
162
|
-
|
|
163
|
-
<Link href="/users">Users</Link>
|
|
164
|
-
|
|
165
|
-
<Link href="/users" data={{ page: 2, tag: ['active', 'new'] }}>
|
|
166
|
-
Filtered users
|
|
167
|
-
</Link>
|
|
168
|
-
|
|
169
|
-
<Link href="/users" replace>
|
|
170
|
-
Replace history entry
|
|
171
|
-
</Link>
|
|
172
|
-
|
|
173
|
-
<Link href="/logout" reloadDocument>
|
|
174
|
-
Sign out
|
|
175
|
-
</Link>
|
|
176
|
-
|
|
177
|
-
<Link
|
|
178
|
-
href="/settings"
|
|
179
|
-
onNavigate={(event) => {
|
|
180
|
-
if (!confirm('Leave this page?')) {
|
|
181
|
-
event.preventDefault();
|
|
182
|
-
}
|
|
183
|
-
}}
|
|
184
|
-
>
|
|
185
|
-
Settings
|
|
186
|
-
</Link>
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Props:
|
|
190
|
-
|
|
191
|
-
- `href` — target URL.
|
|
192
|
-
- `method` — `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`. Defaults to `GET`.
|
|
193
|
-
- `data` — for GET links, primitive values are merged into the query string and arrays become repeated keys. `null` and `undefined` are omitted. Nested objects throw. For non-GET links, `data` is sent as the request body.
|
|
194
|
-
- `prefetch` — `false`, `true`, `"hover"`, `"click"`, `"mount"`, `"visible"`, or an array of strategies. Prefetch is GET-only and throws for non-GET links.
|
|
195
|
-
- `cacheFor` — cache TTL for prefetched page data.
|
|
196
|
-
- `preserveScroll` — keep the current scroll position after navigation.
|
|
197
|
-
- `preserveState` — apply the page update without writing a new URL or history entry.
|
|
198
|
-
- `replace` — replace the current history entry instead of pushing a new one.
|
|
199
|
-
- `reloadDocument` — use browser document navigation. GET-only and throws for non-GET links.
|
|
200
|
-
- `viewTransition` — override the adapter-level View Transition setting for this navigation.
|
|
201
|
-
- `onNavigate` — cancellable callback for client-side navigations. It does not run for external links, downloads, modified clicks, non-`_self` targets, `reloadDocument`, or missing router context.
|
|
202
|
-
|
|
203
|
-
## Pages
|
|
204
|
-
|
|
205
|
-
Pages are `.tsx` files in `pages/` with companion `.server.ts` files:
|
|
206
|
-
|
|
207
|
-
```tsx
|
|
208
|
-
export default function Home({ title }: { title: string }) {
|
|
209
|
-
return <h1>{title}</h1>;
|
|
210
|
-
}
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
```ts
|
|
214
|
-
import { defineHandler } from 'void';
|
|
215
|
-
|
|
216
|
-
export const loader = defineHandler(() => ({ title: 'Home' }));
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
## Form
|
|
220
|
-
|
|
221
|
-
Use `Form` with `useForm()` to display action and validation errors automatically. It preserves controlled input after a failed submission and announces errors accessibly. Structured usage-limit responses display the application's message and an optional retry time. Submissions are blocked while pending and before that deadline; inputs remain editable and requests are never retried automatically.
|
|
222
|
-
|
|
223
|
-
`method` selects `post` (the default), `put`, `patch`, or `delete`. Use `renderError` to customize action-error presentation (a snippet in Svelte). Native forms remain available when you manage error presentation yourself.
|
|
224
|
-
|
|
225
|
-
## Tests
|
|
226
|
-
|
|
227
|
-
- `test/unit/link.test.ts`
|
|
228
|
-
- `test/integration/react-pages-client.test.ts`
|
|
229
|
-
|
|
230
|
-
Cross-package SSR/protocol coverage for React pages mode also lives in `packages/void/test/integration/*`.
|
|
25
|
+
Follow [Pages Routing](https://void.cloud/guide/pages-routing/overview) to add pages, then use [loaders](https://void.cloud/guide/pages-routing/loaders), [forms](https://void.cloud/guide/pages-routing/actions-and-forms), and [layouts](https://void.cloud/guide/pages-routing/layouts).
|
|
231
26
|
|
|
232
|
-
|
|
27
|
+
See the [adapter API reference](https://void.cloud/reference/api/pages) for plugin options and runtime helpers, and [TESTING.md](../../TESTING.md) for repository verification.
|
package/dist/plugin.mjs
CHANGED
|
@@ -288,8 +288,13 @@ async function hydrateIsland(el, mod, props) {
|
|
|
288
288
|
const { hydrateRoot } = await import("react-dom/client");
|
|
289
289
|
const React = await import("react");
|
|
290
290
|
const Component = mod.default || mod;
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
function HydratedIsland() {
|
|
292
|
+
React.useLayoutEffect(() => {
|
|
293
|
+
el.setAttribute("data-hydrated", "true");
|
|
294
|
+
}, []);
|
|
295
|
+
return React.createElement(Component, props);
|
|
296
|
+
}
|
|
297
|
+
hydrateRoot(el, React.createElement(HydratedIsland));
|
|
293
298
|
}
|
|
294
299
|
|
|
295
300
|
initIslands(islands, hydrateIsland);
|
|
@@ -207,7 +207,10 @@ function App({ engine, router, viewTransitions, navigationFallback: NavigationFa
|
|
|
207
207
|
layouts: engine.initialLayouts
|
|
208
208
|
}),
|
|
209
209
|
abort() {},
|
|
210
|
-
async commit() {
|
|
210
|
+
async commit() {
|
|
211
|
+
engine.commitInitialRender();
|
|
212
|
+
document.getElementById("app").setAttribute("data-hydrated", "true");
|
|
213
|
+
},
|
|
211
214
|
dispose() {}
|
|
212
215
|
}), [engine, initialPageData]);
|
|
213
216
|
const [state, dispatch] = useReducer(appReducer, {
|
|
@@ -410,7 +413,6 @@ async function startReactPages(config) {
|
|
|
410
413
|
viewTransitionWaiters.clear();
|
|
411
414
|
pendingVisitSettlers.clear();
|
|
412
415
|
committedResources.clear();
|
|
413
|
-
committedResources.add(0);
|
|
414
416
|
activePendingNavigationId = null;
|
|
415
417
|
const engine = await createPageNavigationEngine({
|
|
416
418
|
adapter: {
|
|
@@ -447,7 +449,6 @@ async function startReactPages(config) {
|
|
|
447
449
|
if (!isSsr) createRoot(appEl).render(app);
|
|
448
450
|
else hydrateRoot(appEl, app);
|
|
449
451
|
});
|
|
450
|
-
appEl.setAttribute("data-hydrated", "true");
|
|
451
452
|
engine.start();
|
|
452
453
|
return { router };
|
|
453
454
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/voidzero-dev/void.git",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@vitejs/plugin-react": "^6.1.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/node": "^26.6.
|
|
38
|
+
"@types/node": "^26.6.4",
|
|
39
39
|
"@types/react": "^19.3.0",
|
|
40
40
|
"@types/react-dom": "^19.3.0",
|
|
41
41
|
"pathslash": "^0.1.0",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"react-dom": "^19.3.0",
|
|
44
44
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
|
|
45
45
|
"vite-plus": "1.0.0",
|
|
46
|
-
"void": "0.
|
|
46
|
+
"void": "0.26.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"react": "^19.0.0",
|
|
50
50
|
"react-dom": "^19.0.0",
|
|
51
51
|
"vite": "^8.0.0",
|
|
52
|
-
"void": "0.
|
|
52
|
+
"void": "0.26.0"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "vp pack",
|