Package not found. Please check the package name and try again.

@void/react 0.0.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 ADDED
@@ -0,0 +1,216 @@
1
+ # @void/react
2
+
3
+ React adapter for Void Pages mode.
4
+
5
+ ## Setup
6
+
7
+ `@void/react` requires React 19.
8
+
9
+ ```ts
10
+ import { defineConfig } from 'vite';
11
+ import { voidPlugin } from 'void';
12
+ import { voidReact } from '@void/react/plugin';
13
+
14
+ export default defineConfig({
15
+ plugins: [voidPlugin(), voidReact()],
16
+ });
17
+ ```
18
+
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
+ - `useNavigation()`
40
+ - `useShared()`
41
+ - `useForm()`
42
+ - `useIslandForm()`
43
+ - `action()`
44
+
45
+ ### Async React Routing
46
+
47
+ React Pages navigations are scheduled as React transitions. Route data,
48
+ component modules, layouts, and deferred props are prepared as resources, then
49
+ revealed through Suspense so the current screen can stay visible while the next
50
+ page loads.
51
+
52
+ ```tsx
53
+ import { Suspense, use } from 'react';
54
+ import { useNavigation, useRouter } from '@void/react';
55
+ import type { Props } from './dashboard.server';
56
+
57
+ function Usage({ usage }: Pick<Props, 'usage'>) {
58
+ const resolved = use(usage);
59
+ return <p>{resolved.requests} requests</p>;
60
+ }
61
+
62
+ export default function Dashboard({ usage }: Props) {
63
+ const router = useRouter();
64
+ const navigation = useNavigation();
65
+
66
+ return (
67
+ <>
68
+ <button onClick={() => router.refresh()} disabled={navigation.state !== 'idle'}>
69
+ Refresh
70
+ </button>
71
+ <Suspense fallback={<p>Loading usage...</p>}>
72
+ <Usage usage={usage} />
73
+ </Suspense>
74
+ </>
75
+ );
76
+ }
77
+ ```
78
+
79
+ Breaking change: React deferred props returned from `defer()` are consumed as
80
+ promises with `use()` under a Suspense boundary. The old
81
+ `prop.loading` / `prop.value` shape remains the runtime shape for the Vue,
82
+ Svelte, and Solid adapters.
83
+
84
+ SSR uses React 19's streaming renderer. Deferred props render the nearest
85
+ Suspense fallback in the initial shell, then Void streams the resolved values to
86
+ the hydrated client. Put deferred reads behind Suspense, and use a normal React
87
+ error boundary around that Suspense boundary when rejected deferred values need a
88
+ custom error state.
89
+
90
+ For explicit prop annotations in React pages, import `Deferred` from
91
+ `@void/react`:
92
+
93
+ ```ts
94
+ import type { Deferred } from '@void/react';
95
+
96
+ export interface Props {
97
+ usage: Deferred<{ requests: number }>;
98
+ }
99
+ ```
100
+
101
+ ### Forms and Actions
102
+
103
+ `useForm()` is built on React Actions. Prefer native form submission with
104
+ `form.post`, `form.put`, `form.patch`, or `form.delete`; use `useFormStatus()`
105
+ inside the form for button pending state.
106
+
107
+ ```tsx
108
+ import { useForm } from '@void/react';
109
+ import { useFormStatus } from 'react-dom';
110
+
111
+ function SubmitButton() {
112
+ const { pending } = useFormStatus();
113
+ return <button disabled={pending}>Save</button>;
114
+ }
115
+
116
+ export default function CreateUser() {
117
+ const form = useForm('/users', { name: '', email: '' });
118
+
119
+ return (
120
+ <form action={form.post}>
121
+ <input
122
+ name="name"
123
+ value={form.data.name}
124
+ onChange={(e) => form.setData('name', e.target.value)}
125
+ />
126
+ {form.errors.name && <span>{form.errors.name}</span>}
127
+ {form.error && <p>{form.error.message}</p>}
128
+ <SubmitButton />
129
+ </form>
130
+ );
131
+ }
132
+ ```
133
+
134
+ `form.post`, `form.put`, `form.patch`, and `form.delete` are native React form
135
+ actions. Use `action()` for the awaitable imperative escape hatch; it returns
136
+ `{ ok: true, pageData }` or `{ ok: false, error }` for call-site action errors,
137
+ while boundary-class failures are thrown. `action()` uses `POST` by default and
138
+ accepts `{ data, method, params }`.
139
+
140
+ ### `useRouter`
141
+
142
+ `useRouter()` returns the Void Router. `visit()`, `refresh()`, and `prefetch()`
143
+ are awaitable; `prefetch()` resolves when the page data has been cached.
144
+
145
+ ### `Link`
146
+
147
+ `Link` is the React Pages navigation component. GET links render an `<a>` and
148
+ client-side navigate when the click is safe to intercept. Mutation links render a
149
+ `<button type="button">` and submit through the Void Router.
150
+
151
+ ```tsx
152
+ import { Link } from '@void/react';
153
+
154
+ <Link href="/users">Users</Link>
155
+
156
+ <Link href="/users" data={{ page: 2, tag: ['active', 'new'] }}>
157
+ Filtered users
158
+ </Link>
159
+
160
+ <Link href="/users" replace>
161
+ Replace history entry
162
+ </Link>
163
+
164
+ <Link href="/logout" reloadDocument>
165
+ Sign out
166
+ </Link>
167
+
168
+ <Link
169
+ href="/settings"
170
+ onNavigate={(event) => {
171
+ if (!confirm('Leave this page?')) {
172
+ event.preventDefault();
173
+ }
174
+ }}
175
+ >
176
+ Settings
177
+ </Link>
178
+ ```
179
+
180
+ Props:
181
+
182
+ - `href` — target URL.
183
+ - `method` — `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`. Defaults to `GET`.
184
+ - `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.
185
+ - `prefetch` — `false`, `true`, `"hover"`, `"click"`, `"mount"`, `"visible"`, or an array of strategies. Prefetch is GET-only and throws for non-GET links.
186
+ - `cacheFor` — cache TTL for prefetched page data.
187
+ - `preserveScroll` — keep the current scroll position after navigation.
188
+ - `preserveState` — apply the page update without writing a new URL or history entry.
189
+ - `replace` — replace the current history entry instead of pushing a new one.
190
+ - `reloadDocument` — use browser document navigation. GET-only and throws for non-GET links.
191
+ - `viewTransition` — override the adapter-level View Transition setting for this navigation.
192
+ - `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.
193
+
194
+ ## Pages
195
+
196
+ Pages are `.tsx` files in `pages/` with companion `.server.ts` files:
197
+
198
+ ```tsx
199
+ export default function Home({ title }: { title: string }) {
200
+ return <h1>{title}</h1>;
201
+ }
202
+ ```
203
+
204
+ ```ts
205
+ import { defineHandler } from 'void';
206
+
207
+ export const loader = defineHandler(() => ({ title: 'Home' }));
208
+ ```
209
+
210
+ ## Tests
211
+
212
+ - `test/unit/use-form.test.ts`
213
+ - `test/unit/link.test.ts`
214
+ - `test/integration/react-pages-client.test.ts`
215
+
216
+ Cross-package SSR/protocol coverage for React pages mode also lives in `framework/packages/void/test/integration/*`.
@@ -0,0 +1,29 @@
1
+ import { submitAction } from "void/pages-client";
2
+ //#region src/runtime/action.ts
3
+ let _router = null;
4
+ /** Called during app initialization to store the router instance */
5
+ function setActionRouter(router) {
6
+ _router = router;
7
+ }
8
+ /** Programmatic one-shot page action call with Inertia page update */
9
+ async function action(url, ...args) {
10
+ if (!_router) throw new Error("action(): called before router initialization.");
11
+ const options = args[0];
12
+ const data = options?.data;
13
+ const method = options?.method || "POST";
14
+ let resolvedUrl = url;
15
+ let actionQuery = "";
16
+ const qIdx = resolvedUrl.indexOf("?");
17
+ if (qIdx !== -1) {
18
+ actionQuery = resolvedUrl.slice(qIdx);
19
+ resolvedUrl = resolvedUrl.slice(0, qIdx);
20
+ }
21
+ if (options?.params) for (const [key, value] of Object.entries(options.params)) resolvedUrl = resolvedUrl.replace(`:${key}`, encodeURIComponent(value));
22
+ resolvedUrl = resolvedUrl + actionQuery;
23
+ return submitAction(_router, resolvedUrl, {
24
+ method,
25
+ data
26
+ });
27
+ }
28
+ //#endregion
29
+ export { setActionRouter as n, action as t };
@@ -0,0 +1,9 @@
1
+ import { createContext } from "react";
2
+ import { idleNavigationState } from "void/pages-client";
3
+ //#region src/runtime/context.ts
4
+ const SharedContext = createContext(null);
5
+ const ErrorsContext = createContext(null);
6
+ const RouterContext = createContext(null);
7
+ const NavigationContext = createContext(idleNavigationState());
8
+ //#endregion
9
+ export { SharedContext as i, NavigationContext as n, RouterContext as r, ErrorsContext as t };
@@ -0,0 +1,125 @@
1
+ import { AnchorHTMLAttributes, ButtonHTMLAttributes, Context, JSX } from "react";
2
+ import { ActionResult, NavigationState, VoidActionError, VoidRouter } from "void/pages-client";
3
+ import { CloudContextVariables, InferProps } from "void";
4
+ import { ActionFormOptions, ActionUrl, ResolveActionBody, ResolveActionParams } from "void/routes";
5
+
6
+ //#region src/runtime/link.d.ts
7
+ type PrefetchStrategy = "hover" | "click" | "mount" | "visible";
8
+ type LinkMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
9
+ type CommonProps = {
10
+ href: string;
11
+ method?: LinkMethod | Lowercase<LinkMethod>;
12
+ data?: Record<string, unknown>;
13
+ preserveScroll?: boolean;
14
+ preserveState?: boolean;
15
+ replace?: boolean;
16
+ reloadDocument?: boolean;
17
+ viewTransition?: boolean;
18
+ prefetch?: boolean | PrefetchStrategy | Array<PrefetchStrategy>;
19
+ cacheFor?: number | string | [string, string];
20
+ onNavigate?: (event: {
21
+ preventDefault(): void;
22
+ }) => void;
23
+ };
24
+ type LinkProps = CommonProps & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof CommonProps> & Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof CommonProps>;
25
+ declare function Link({
26
+ href,
27
+ method,
28
+ data,
29
+ preserveScroll,
30
+ preserveState,
31
+ replace,
32
+ reloadDocument,
33
+ viewTransition,
34
+ prefetch: prefetchProp,
35
+ cacheFor,
36
+ onNavigate,
37
+ children,
38
+ style,
39
+ onClick: onClickProp,
40
+ onMouseEnter: onMouseEnterProp,
41
+ onMouseLeave: onMouseLeaveProp,
42
+ onFocus: onFocusProp,
43
+ onMouseDown: onMouseDownProp,
44
+ onTouchStart: onTouchStartProp,
45
+ ...rest
46
+ }: LinkProps): JSX.Element;
47
+ //#endregion
48
+ //#region src/runtime/use-router.d.ts
49
+ declare function useRouter(): VoidRouter;
50
+ //#endregion
51
+ //#region src/runtime/use-shared.d.ts
52
+ declare function useShared<T = CloudContextVariables["shared"]>(): T;
53
+ //#endregion
54
+ //#region src/runtime/use-form.d.ts
55
+ declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U>): {
56
+ data: ResolveActionBody<U>;
57
+ setData: <K extends keyof ResolveActionBody<U> & string>(field: K, value: ResolveActionBody<U>[K]) => void;
58
+ errors: Partial<Record<keyof ResolveActionBody<U> & string, string>>;
59
+ error: VoidActionError | null;
60
+ pending: boolean;
61
+ hasChanges: boolean;
62
+ wasSuccessful: boolean;
63
+ recentlySuccessful: boolean;
64
+ post(formData: FormData): void;
65
+ put(formData: FormData): void;
66
+ patch(formData: FormData): void;
67
+ delete(formData: FormData): void;
68
+ reset(...fields: Array<keyof ResolveActionBody<U> & string>): void;
69
+ clearErrors(...fields: Array<keyof ResolveActionBody<U> & string>): void;
70
+ clearError(): void;
71
+ };
72
+ //#endregion
73
+ //#region src/runtime/use-navigation.d.ts
74
+ declare function useNavigation(): NavigationState;
75
+ //#endregion
76
+ //#region src/runtime/use-island-form.d.ts
77
+ interface IslandFormReturn<T extends Record<string, unknown>> {
78
+ data: T;
79
+ setData: <K extends keyof T & string>(field: K, value: T[K]) => void;
80
+ errors: Record<string, string>;
81
+ error: VoidActionError | null;
82
+ pending: boolean;
83
+ hasChanges: boolean;
84
+ wasSuccessful: boolean;
85
+ recentlySuccessful: boolean;
86
+ reset: (...fields: Array<keyof T & string>) => void;
87
+ clearErrors: (...fields: Array<string>) => void;
88
+ clearError: () => void;
89
+ post: (url: string) => Promise<void>;
90
+ put: (url: string) => Promise<void>;
91
+ patch: (url: string) => Promise<void>;
92
+ delete: (url: string) => Promise<void>;
93
+ }
94
+ /**
95
+ * Form hook for island pages. Uses fetch + page reload instead of the
96
+ * Void Router (which is not available in island mode).
97
+ */
98
+ declare function useIslandForm<T extends Record<string, unknown>>(defaults: T): IslandFormReturn<T>;
99
+ //#endregion
100
+ //#region src/runtime/action.d.ts
101
+ type ActionMethod = "POST" | "PUT" | "PATCH" | "DELETE";
102
+ type HasParams<U extends string> = [ResolveActionParams<U>] extends [never] ? false : true;
103
+ type ActionOptions<U extends string> = {
104
+ data?: ResolveActionBody<U>;
105
+ method?: ActionMethod;
106
+ } & (HasParams<U> extends true ? {
107
+ params: ResolveActionParams<U>;
108
+ } : {
109
+ params?: never;
110
+ });
111
+ /** Called during app initialization to store the router instance */
112
+ declare function setActionRouter(router: VoidRouter): void;
113
+ /** Programmatic one-shot page action call with Inertia page update */
114
+ declare function action<U extends ActionUrl & string>(url: U, ...args: HasParams<U> extends true ? [options: ActionOptions<U>] : [options?: ActionOptions<U>]): Promise<ActionResult>;
115
+ //#endregion
116
+ //#region src/runtime/context.d.ts
117
+ declare const SharedContext: Context<Record<string, unknown> | null>;
118
+ declare const ErrorsContext: Context<Record<string, string> | null>;
119
+ declare const RouterContext: Context<VoidRouter | null>;
120
+ declare const NavigationContext: Context<NavigationState>;
121
+ //#endregion
122
+ //#region src/runtime/index.d.ts
123
+ type Deferred<T> = Promise<T>;
124
+ //#endregion
125
+ export { RouterContext as a, setActionRouter as c, useForm as d, useShared as f, NavigationContext as i, useIslandForm as l, Link as m, InferProps as n, SharedContext as o, useRouter as p, ErrorsContext as r, action as s, Deferred as t, useNavigation as u };
@@ -0,0 +1,2 @@
1
+ import { a as RouterContext, c as setActionRouter, d as useForm, f as useShared, i as NavigationContext, l as useIslandForm, m as Link, n as InferProps, o as SharedContext, p as useRouter, r as ErrorsContext, s as action, t as Deferred, u as useNavigation } from "./index-6hxGVqsE.mjs";
2
+ export { Deferred, ErrorsContext, InferProps, Link, NavigationContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useRouter, useShared };
package/dist/index.mjs ADDED
@@ -0,0 +1,4 @@
1
+ import { i as SharedContext, n as NavigationContext, r as RouterContext, t as ErrorsContext } from "./context-BCeFV8Jy.mjs";
2
+ import { a as useRouter, i as useShared, n as useNavigation, o as Link, r as useForm, t as useIslandForm } from "./runtime-C24S_Dlg.mjs";
3
+ import { n as setActionRouter, t as action } from "./action-BFWtavbf.mjs";
4
+ export { ErrorsContext, Link, NavigationContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useRouter, useShared };
@@ -0,0 +1,16 @@
1
+ import react from "@vitejs/plugin-react";
2
+ import { Plugin } from "vite";
3
+
4
+ //#region src/plugin.d.ts
5
+ interface PrefetchConfig {
6
+ hoverDelay?: number;
7
+ cacheFor?: number | string | [string, string];
8
+ }
9
+ interface VoidReactOptions {
10
+ react?: Parameters<typeof react>[0];
11
+ viewTransitions?: boolean;
12
+ prefetch?: PrefetchConfig;
13
+ }
14
+ declare function voidReact(options?: VoidReactOptions): Array<Plugin>;
15
+ //#endregion
16
+ export { VoidReactOptions, voidReact };