@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 CHANGED
@@ -1,10 +1,16 @@
1
1
  # @void/react
2
2
 
3
- React adapter for Void Pages mode.
3
+ Build server-rendered Void pages with React, server loaders, and typed forms. Requires React 19.
4
4
 
5
5
  ## Setup
6
6
 
7
- `@void/react` requires React 19.
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
- ## Plugin
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
- The [shared client](../../test/pages-client-contract.ts) and [island](../../test/pages-island-contract.ts) contracts run against this adapter in both dev and preview. Shared form equality is tested directly in [Void](../void/test/unit/form-value-equality.test.ts); tests do not copy the hook implementation.
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
- hydrateRoot(el, React.createElement(Component, props));
292
- el.setAttribute("data-hydrated", "true");
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.24.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.3",
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.24.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.24.0"
52
+ "void": "0.26.0"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "vp pack",