@pylonsync/react 0.3.307 → 0.3.309
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/dist/Link.d.ts +22 -1
- package/dist/index.d.ts +1 -1
- package/dist/useRouter.d.ts +46 -0
- package/package.json +3 -3
- package/src/Link.tsx +38 -1
- package/src/index.ts +2 -0
- package/src/useRouter.ts +94 -1
package/dist/Link.d.ts
CHANGED
|
@@ -6,11 +6,19 @@ declare global {
|
|
|
6
6
|
navigate: (href: string, opts?: {
|
|
7
7
|
push?: boolean;
|
|
8
8
|
replace?: boolean;
|
|
9
|
+
seed?: unknown;
|
|
9
10
|
}) => Promise<void>;
|
|
10
11
|
/** Current route's dynamic params (read by useParams). A getter on the
|
|
11
12
|
* runtime side, so it always reflects the latest navigation. */
|
|
12
13
|
readonly params?: Record<string, string>;
|
|
14
|
+
/** Seed the active navigation was started with (read by useRouteSeed).
|
|
15
|
+
* A getter on the runtime side; null outside an optimistic nav. */
|
|
16
|
+
readonly seed?: unknown;
|
|
13
17
|
};
|
|
18
|
+
/** WeakMap of <Link seed> anchor element → its seed value. Populated by
|
|
19
|
+
* <Link> and read by the runtime's global click handler so it can pass the
|
|
20
|
+
* seed to navigate() for an optimistic first paint. */
|
|
21
|
+
__pylonLinkSeeds?: WeakMap<Element, unknown>;
|
|
14
22
|
}
|
|
15
23
|
}
|
|
16
24
|
export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
@@ -22,6 +30,19 @@ export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorEle
|
|
|
22
30
|
* is unlikely to follow — pagination tail, etc.).
|
|
23
31
|
*/
|
|
24
32
|
prefetch?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Data you already have for the destination (e.g. the list item you clicked).
|
|
35
|
+
* When set, Pylon paints the destination page's Suspense fallback with this
|
|
36
|
+
* data the instant the link is clicked — before the SSR fetch resolves — so
|
|
37
|
+
* above-the-fold content appears immediately instead of a skeleton or a delay.
|
|
38
|
+
* Read it on the destination page with `useRouteSeed<T>()`.
|
|
39
|
+
*
|
|
40
|
+
* Degrades gracefully: no seed, or a route Pylon can't resolve on the client,
|
|
41
|
+
* simply falls back to normal navigation. Best for detail routes whose page
|
|
42
|
+
* keeps its `serverData` reads inside a `<Suspense>` (so the seeded fallback
|
|
43
|
+
* has somewhere to show).
|
|
44
|
+
*/
|
|
45
|
+
seed?: unknown;
|
|
25
46
|
children?: React.ReactNode;
|
|
26
47
|
}
|
|
27
|
-
export declare function Link({ href, prefetch, children, ...rest }: LinkProps): React.JSX.Element;
|
|
48
|
+
export declare function Link({ href, prefetch, seed, children, ...rest }: LinkProps): React.JSX.Element;
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type { ImageProps } from "./Image";
|
|
|
7
7
|
export { Form } from "./Form";
|
|
8
8
|
export type { FormProps } from "./Form";
|
|
9
9
|
export type { PageProps, PageAuth, ServerData, SsrResponse, SsrCookieOptions, Metadata, GenerateMetadata, Sitemap, SitemapEntry, Robots, RobotsRule, RouteSegmentConfig, ErrorBoundaryProps, NotFoundProps, FormFields, FormDb, FormRequest, RouteHandler, RawRouteHandler, RawResponse, } from "./ssr";
|
|
10
|
-
export { useRouter, useSearchParams, usePathname, useParams, redirect, notFound, NotFoundError, } from "./useRouter";
|
|
10
|
+
export { useRouter, useSearchParams, usePathname, useParams, useRouteSeed, useRouteData, redirect, notFound, NotFoundError, } from "./useRouter";
|
|
11
11
|
export type { PylonRouter } from "./useRouter";
|
|
12
12
|
import { type Storage as PylonStorage } from "@pylonsync/sync";
|
|
13
13
|
export { useQuery, useQueryOne, useReactiveQuery, useMutation, useInfiniteQuery, usePaginatedQuery, useEntityMutation, useAction, useQueryRaw, useQueryOneRaw, useLiveList, useLiveRow, useInsert, useUpdate, useDelete, useFn, useAggregate, useSearch, } from "./hooks";
|
package/dist/useRouter.d.ts
CHANGED
|
@@ -27,6 +27,52 @@ export declare function usePathname(): string;
|
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
export declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
|
|
30
|
+
/**
|
|
31
|
+
* The seed passed to the `<Link seed>` that started the current navigation, for
|
|
32
|
+
* an instant optimistic first paint. Returns the seed while the destination's
|
|
33
|
+
* data is still loading, then `null` once the real server render lands (and on
|
|
34
|
+
* hard loads / seedless navs). Use it as the page's Suspense fallback so
|
|
35
|
+
* above-the-fold content shows immediately instead of a skeleton:
|
|
36
|
+
*
|
|
37
|
+
* ```tsx
|
|
38
|
+
* export default function Page({ params, serverData }: PageProps<{ slug: string }>) {
|
|
39
|
+
* const seed = useRouteSeed<Product>();
|
|
40
|
+
* return (
|
|
41
|
+
* <Suspense fallback={seed ? <ProductView product={seed} pending /> : <Skeleton />}>
|
|
42
|
+
* <ProductDetail serverData={serverData} slug={params.slug} />
|
|
43
|
+
* </Suspense>
|
|
44
|
+
* );
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function useRouteSeed<T = unknown>(): T | null;
|
|
49
|
+
/**
|
|
50
|
+
* Load a page's primary data with an optimistic first paint from `<Link seed>`.
|
|
51
|
+
* This is the flash-free way to consume a seed — prefer it over reading
|
|
52
|
+
* `useRouteSeed()` into a Suspense fallback (a fallback→content transition
|
|
53
|
+
* remounts, which flickers).
|
|
54
|
+
*
|
|
55
|
+
* - Hard load / SSR (no seed): suspends on `loader()`, so the server renders +
|
|
56
|
+
* streams the real content and the client hydrates from the pre-fulfilled
|
|
57
|
+
* serverData cache. Wrap the calling component in `<Suspense>`.
|
|
58
|
+
* - Optimistic client nav (a `<Link seed>` was clicked): returns the seed
|
|
59
|
+
* immediately as real content — NO Suspense fallback — then swaps to the
|
|
60
|
+
* resolved `loader()` value IN PLACE (same component instance), so there is no
|
|
61
|
+
* remount between the seed and the authoritative row. No flash.
|
|
62
|
+
*
|
|
63
|
+
* `deps` drive the background reload — pass the values `loader` closes over
|
|
64
|
+
* (e.g. `[serverData, slug]`). Key the component by its dynamic route param if
|
|
65
|
+
* it serves both seeded and hard-loaded requests, so each navigation mounts a
|
|
66
|
+
* fresh instance in the right mode.
|
|
67
|
+
*
|
|
68
|
+
* ```tsx
|
|
69
|
+
* function Detail({ serverData, slug }: { serverData: ServerData; slug: string }) {
|
|
70
|
+
* const product = useRouteData(() => loadProduct(serverData, slug), [serverData, slug]);
|
|
71
|
+
* return product ? <ProductView product={product} /> : <NotFound />;
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function useRouteData<T>(loader: () => Promise<T> | T, deps: readonly unknown[]): T | null;
|
|
30
76
|
/**
|
|
31
77
|
* Client-side redirect — replaces the current history entry with `href`.
|
|
32
78
|
* Drop-in for Next's `redirect` when called from a client component
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.309",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"prepack": "bun run build"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@pylonsync/sdk": "0.3.
|
|
17
|
-
"@pylonsync/sync": "0.3.
|
|
16
|
+
"@pylonsync/sdk": "0.3.309",
|
|
17
|
+
"@pylonsync/sync": "0.3.309"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"react": ">=19.0.0"
|
package/src/Link.tsx
CHANGED
|
@@ -32,12 +32,19 @@ declare global {
|
|
|
32
32
|
prefetch: (href: string) => Promise<void>;
|
|
33
33
|
navigate: (
|
|
34
34
|
href: string,
|
|
35
|
-
opts?: { push?: boolean; replace?: boolean },
|
|
35
|
+
opts?: { push?: boolean; replace?: boolean; seed?: unknown },
|
|
36
36
|
) => Promise<void>;
|
|
37
37
|
/** Current route's dynamic params (read by useParams). A getter on the
|
|
38
38
|
* runtime side, so it always reflects the latest navigation. */
|
|
39
39
|
readonly params?: Record<string, string>;
|
|
40
|
+
/** Seed the active navigation was started with (read by useRouteSeed).
|
|
41
|
+
* A getter on the runtime side; null outside an optimistic nav. */
|
|
42
|
+
readonly seed?: unknown;
|
|
40
43
|
};
|
|
44
|
+
/** WeakMap of <Link seed> anchor element → its seed value. Populated by
|
|
45
|
+
* <Link> and read by the runtime's global click handler so it can pass the
|
|
46
|
+
* seed to navigate() for an optimistic first paint. */
|
|
47
|
+
__pylonLinkSeeds?: WeakMap<Element, unknown>;
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
|
|
@@ -51,17 +58,47 @@ export interface LinkProps
|
|
|
51
58
|
* is unlikely to follow — pagination tail, etc.).
|
|
52
59
|
*/
|
|
53
60
|
prefetch?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Data you already have for the destination (e.g. the list item you clicked).
|
|
63
|
+
* When set, Pylon paints the destination page's Suspense fallback with this
|
|
64
|
+
* data the instant the link is clicked — before the SSR fetch resolves — so
|
|
65
|
+
* above-the-fold content appears immediately instead of a skeleton or a delay.
|
|
66
|
+
* Read it on the destination page with `useRouteSeed<T>()`.
|
|
67
|
+
*
|
|
68
|
+
* Degrades gracefully: no seed, or a route Pylon can't resolve on the client,
|
|
69
|
+
* simply falls back to normal navigation. Best for detail routes whose page
|
|
70
|
+
* keeps its `serverData` reads inside a `<Suspense>` (so the seeded fallback
|
|
71
|
+
* has somewhere to show).
|
|
72
|
+
*/
|
|
73
|
+
seed?: unknown;
|
|
54
74
|
children?: React.ReactNode;
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
export function Link({
|
|
58
78
|
href,
|
|
59
79
|
prefetch = true,
|
|
80
|
+
seed,
|
|
60
81
|
children,
|
|
61
82
|
...rest
|
|
62
83
|
}: LinkProps) {
|
|
63
84
|
const ref = useRef<HTMLAnchorElement>(null);
|
|
64
85
|
|
|
86
|
+
// Register the seed keyed by this anchor element so the runtime's global
|
|
87
|
+
// click handler can find it and hand it to navigate(). A WeakMap keeps it
|
|
88
|
+
// GC-safe (no leak when the link unmounts) and off the DOM node itself.
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (typeof window === "undefined") return;
|
|
91
|
+
const el = ref.current;
|
|
92
|
+
if (!el || seed === undefined) return;
|
|
93
|
+
const reg =
|
|
94
|
+
window.__pylonLinkSeeds ||
|
|
95
|
+
(window.__pylonLinkSeeds = new WeakMap<Element, unknown>());
|
|
96
|
+
reg.set(el, seed);
|
|
97
|
+
return () => {
|
|
98
|
+
reg.delete(el);
|
|
99
|
+
};
|
|
100
|
+
}, [seed]);
|
|
101
|
+
|
|
65
102
|
useEffect(() => {
|
|
66
103
|
if (!prefetch || !ref.current) return;
|
|
67
104
|
if (typeof window === "undefined") return;
|
package/src/index.ts
CHANGED
package/src/useRouter.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// `searchParams` PROPS the runtime already hands every page (see PageProps);
|
|
13
13
|
// the hooks exist for deep children that need to react to client navigation
|
|
14
14
|
// without prop-drilling.
|
|
15
|
-
import { useMemo, useSyncExternalStore } from "react";
|
|
15
|
+
import { use, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
16
16
|
|
|
17
17
|
// `Window.__pylon` is globally augmented in ./Link (same package, ambient).
|
|
18
18
|
|
|
@@ -114,6 +114,99 @@ export function useParams<
|
|
|
114
114
|
) as T;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// The seed for the active navigation, stashed on `window.__pylon.seed` by the
|
|
118
|
+
// runtime when a <Link seed> is clicked. A stable reference for the whole nav
|
|
119
|
+
// (set once at nav start), which useSyncExternalStore requires. Null otherwise.
|
|
120
|
+
function seedClientSnapshot(): unknown {
|
|
121
|
+
return (typeof window !== "undefined" && window.__pylon?.seed) || null;
|
|
122
|
+
}
|
|
123
|
+
function seedServerSnapshot(): unknown {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The seed passed to the `<Link seed>` that started the current navigation, for
|
|
129
|
+
* an instant optimistic first paint. Returns the seed while the destination's
|
|
130
|
+
* data is still loading, then `null` once the real server render lands (and on
|
|
131
|
+
* hard loads / seedless navs). Use it as the page's Suspense fallback so
|
|
132
|
+
* above-the-fold content shows immediately instead of a skeleton:
|
|
133
|
+
*
|
|
134
|
+
* ```tsx
|
|
135
|
+
* export default function Page({ params, serverData }: PageProps<{ slug: string }>) {
|
|
136
|
+
* const seed = useRouteSeed<Product>();
|
|
137
|
+
* return (
|
|
138
|
+
* <Suspense fallback={seed ? <ProductView product={seed} pending /> : <Skeleton />}>
|
|
139
|
+
* <ProductDetail serverData={serverData} slug={params.slug} />
|
|
140
|
+
* </Suspense>
|
|
141
|
+
* );
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export function useRouteSeed<T = unknown>(): T | null {
|
|
146
|
+
return useSyncExternalStore(
|
|
147
|
+
subscribe,
|
|
148
|
+
seedClientSnapshot,
|
|
149
|
+
seedServerSnapshot,
|
|
150
|
+
) as T | null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Load a page's primary data with an optimistic first paint from `<Link seed>`.
|
|
155
|
+
* This is the flash-free way to consume a seed — prefer it over reading
|
|
156
|
+
* `useRouteSeed()` into a Suspense fallback (a fallback→content transition
|
|
157
|
+
* remounts, which flickers).
|
|
158
|
+
*
|
|
159
|
+
* - Hard load / SSR (no seed): suspends on `loader()`, so the server renders +
|
|
160
|
+
* streams the real content and the client hydrates from the pre-fulfilled
|
|
161
|
+
* serverData cache. Wrap the calling component in `<Suspense>`.
|
|
162
|
+
* - Optimistic client nav (a `<Link seed>` was clicked): returns the seed
|
|
163
|
+
* immediately as real content — NO Suspense fallback — then swaps to the
|
|
164
|
+
* resolved `loader()` value IN PLACE (same component instance), so there is no
|
|
165
|
+
* remount between the seed and the authoritative row. No flash.
|
|
166
|
+
*
|
|
167
|
+
* `deps` drive the background reload — pass the values `loader` closes over
|
|
168
|
+
* (e.g. `[serverData, slug]`). Key the component by its dynamic route param if
|
|
169
|
+
* it serves both seeded and hard-loaded requests, so each navigation mounts a
|
|
170
|
+
* fresh instance in the right mode.
|
|
171
|
+
*
|
|
172
|
+
* ```tsx
|
|
173
|
+
* function Detail({ serverData, slug }: { serverData: ServerData; slug: string }) {
|
|
174
|
+
* const product = useRouteData(() => loadProduct(serverData, slug), [serverData, slug]);
|
|
175
|
+
* return product ? <ProductView product={product} /> : <NotFound />;
|
|
176
|
+
* }
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export function useRouteData<T>(
|
|
180
|
+
loader: () => Promise<T> | T,
|
|
181
|
+
deps: readonly unknown[],
|
|
182
|
+
): T | null {
|
|
183
|
+
const seed = useRouteSeed<T>();
|
|
184
|
+
// Fix the mode at first render so the hook calls below stay stable even if the
|
|
185
|
+
// seed changes on a later same-instance nav (rules of hooks). `use()` may be
|
|
186
|
+
// called conditionally, but useState/useEffect may not — hence the ref.
|
|
187
|
+
const optimistic = useRef(seed != null).current;
|
|
188
|
+
const [data, setData] = useState<T | null>(optimistic ? seed : null);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!optimistic) return;
|
|
191
|
+
let live = true;
|
|
192
|
+
Promise.resolve(loader()).then((v) => {
|
|
193
|
+
if (live && v != null) setData(v as T);
|
|
194
|
+
});
|
|
195
|
+
return () => {
|
|
196
|
+
live = false;
|
|
197
|
+
};
|
|
198
|
+
// loader is intentionally excluded; `deps` are the reload trigger.
|
|
199
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
200
|
+
}, deps);
|
|
201
|
+
if (!optimistic) {
|
|
202
|
+
// SSR + hard load + non-seeded nav: suspend so the server streams the real
|
|
203
|
+
// content and the client hydrates it synchronously from the pre-fulfilled
|
|
204
|
+
// serverData cache (matches the server HTML — no mismatch).
|
|
205
|
+
return use(Promise.resolve(loader())) as T;
|
|
206
|
+
}
|
|
207
|
+
return data ?? seed;
|
|
208
|
+
}
|
|
209
|
+
|
|
117
210
|
/**
|
|
118
211
|
* Client-side redirect — replaces the current history entry with `href`.
|
|
119
212
|
* Drop-in for Next's `redirect` when called from a client component
|