@tanstack/react-start-client 1.167.3 → 1.168.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/dist/esm/GenericHydrate.d.ts +3 -0
- package/dist/esm/GenericHydrate.js +243 -0
- package/dist/esm/GenericHydrate.js.map +1 -0
- package/dist/esm/Hydrate.d.ts +31 -0
- package/dist/esm/Hydrate.js +34 -0
- package/dist/esm/Hydrate.js.map +1 -0
- package/dist/esm/hydration/generic.d.ts +7 -0
- package/dist/esm/hydration/generic.js +20 -0
- package/dist/esm/hydration/generic.js.map +1 -0
- package/dist/esm/hydration/idle.d.ts +3 -0
- package/dist/esm/hydration/idle.js +12 -0
- package/dist/esm/hydration/idle.js.map +1 -0
- package/dist/esm/hydration/load.d.ts +5 -0
- package/dist/esm/hydration/load.js +33 -0
- package/dist/esm/hydration/load.js.map +1 -0
- package/dist/esm/hydration/never.d.ts +4 -0
- package/dist/esm/hydration/never.js +56 -0
- package/dist/esm/hydration/never.js.map +1 -0
- package/dist/esm/hydration/visible.d.ts +5 -0
- package/dist/esm/hydration/visible.js +94 -0
- package/dist/esm/hydration/visible.js.map +1 -0
- package/dist/esm/hydration.d.ts +7 -0
- package/dist/esm/hydration.js +7 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +3 -1
- package/dist/esm/tests/Hydrate.test-d.d.ts +1 -0
- package/dist/esm/tests/Hydrate.test.d.ts +1 -0
- package/package.json +10 -4
- package/src/GenericHydrate.tsx +436 -0
- package/src/Hydrate.tsx +107 -0
- package/src/hydration/generic.ts +43 -0
- package/src/hydration/idle.ts +22 -0
- package/src/hydration/load.tsx +49 -0
- package/src/hydration/never.tsx +97 -0
- package/src/hydration/visible.tsx +139 -0
- package/src/hydration.ts +22 -0
- package/src/index.tsx +16 -0
- package/src/tests/Hydrate.test-d.tsx +147 -0
- package/src/tests/Hydrate.test.tsx +676 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visible.js","names":[],"sources":["../../../src/hydration/visible.tsx"],"sourcesContent":["'use client'\n\nimport * as React from 'react'\n\nimport { reactUse } from '@tanstack/react-router'\nimport { isServer } from '@tanstack/router-core/isServer'\nimport type {\n HydrationPrefetchStrategy,\n VisibleHydrationOptions,\n} from '@tanstack/start-client-core/hydration'\nimport type {\n HydrateProps,\n InternalHydrateProps,\n ReactHydrationStrategy,\n} from '../Hydrate'\n\ntype VisibleGate = {\n p: Promise<void>\n r: boolean\n s: () => void\n}\n\n/* @__NO_SIDE_EFFECTS__ */\nfunction HydrationBoundary(props: {\n g: VisibleGate\n o?: () => void\n children?: React.ReactNode\n}) {\n const { g, o } = props\n\n if (!g.r) {\n if (!reactUse) {\n throw g.p\n }\n\n reactUse(g.p)\n }\n\n React.useEffect(() => {\n o?.()\n }, [o])\n\n return props.children as React.JSX.Element\n}\n\n/* @__NO_SIDE_EFFECTS__ */\nexport function VisibleHydrate(\n this: ReactHydrationStrategy,\n props: HydrateProps,\n): React.JSX.Element {\n const strategy = this as ReactHydrationStrategy<'visible', true>\n const prefetchStrategy = props.prefetch\n const preload = (props as InternalHydrateProps).p\n const markerRef = React.useRef<HTMLDivElement | null>(null)\n const [gate] = React.useState<VisibleGate>(() => {\n let resolvePromise!: () => void\n const nextGate: VisibleGate = {\n p: new Promise<void>((resolve) => {\n resolvePromise = resolve\n }),\n r: false,\n s: () => {\n nextGate.r = true\n resolvePromise()\n },\n }\n if (\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n isServer ??\n typeof window === 'undefined'\n ) {\n nextGate.s()\n }\n\n return nextGate\n })\n\n React.useEffect(() => {\n if (!preload || typeof prefetchStrategy === 'function') {\n return\n }\n\n return prefetchStrategy?._s?.({\n element: markerRef.current,\n prefetch: preload,\n })\n }, [prefetchStrategy, preload])\n\n React.useEffect(() => {\n if (gate.r) return\n\n return strategy._s?.({\n element: markerRef.current,\n gate: gate as never,\n })\n }, [gate, strategy])\n\n return (\n <div ref={markerRef}>\n <React.Suspense fallback={props.fallback}>\n <HydrationBoundary g={gate} o={props.onHydrated}>\n {props.children}\n </HydrationBoundary>\n </React.Suspense>\n </div>\n )\n}\n\n/* @__NO_SIDE_EFFECTS__ */\nexport function visible(\n options?: VisibleHydrationOptions,\n): ReactHydrationStrategy<'visible', true> &\n HydrationPrefetchStrategy<'visible'> {\n const rootMargin = options?.rootMargin ?? '600px'\n const threshold = options?.threshold ?? 0\n\n return {\n _s: ({ element, gate, prefetch }) => {\n const callback = prefetch || (gate as never as VisibleGate).s\n\n if (!element) {\n callback()\n return\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n if (!entries[0]!.isIntersecting) return\n observer.disconnect()\n callback()\n },\n { rootMargin, threshold },\n )\n observer.observe(element)\n return () => observer.disconnect()\n },\n _h: VisibleHydrate,\n }\n}\n"],"mappings":";;;;;;;AAuBA,SAAS,kBAAkB,OAIxB;CACD,MAAM,EAAE,GAAG,MAAM;AAEjB,KAAI,CAAC,EAAE,GAAG;AACR,MAAI,CAAC,SACH,OAAM,EAAE;AAGV,WAAS,EAAE,EAAE;;AAGf,OAAM,gBAAgB;AACpB,OAAK;IACJ,CAAC,EAAE,CAAC;AAEP,QAAO,MAAM;;;AAIf,SAAgB,eAEd,OACmB;CACnB,MAAM,WAAW;CACjB,MAAM,mBAAmB,MAAM;CAC/B,MAAM,UAAW,MAA+B;CAChD,MAAM,YAAY,MAAM,OAA8B,KAAK;CAC3D,MAAM,CAAC,QAAQ,MAAM,eAA4B;EAC/C,IAAI;EACJ,MAAM,WAAwB;GAC5B,GAAG,IAAI,SAAe,YAAY;AAChC,qBAAiB;KACjB;GACF,GAAG;GACH,SAAS;AACP,aAAS,IAAI;AACb,oBAAgB;;GAEnB;AACD,MAEE,YACA,OAAO,WAAW,YAElB,UAAS,GAAG;AAGd,SAAO;GACP;AAEF,OAAM,gBAAgB;AACpB,MAAI,CAAC,WAAW,OAAO,qBAAqB,WAC1C;AAGF,SAAO,kBAAkB,KAAK;GAC5B,SAAS,UAAU;GACnB,UAAU;GACX,CAAC;IACD,CAAC,kBAAkB,QAAQ,CAAC;AAE/B,OAAM,gBAAgB;AACpB,MAAI,KAAK,EAAG;AAEZ,SAAO,SAAS,KAAK;GACnB,SAAS,UAAU;GACb;GACP,CAAC;IACD,CAAC,MAAM,SAAS,CAAC;AAEpB,QACE,oBAAC,OAAD;EAAK,KAAK;YACR,oBAAC,MAAM,UAAP;GAAgB,UAAU,MAAM;aAC9B,oBAAC,mBAAD;IAAmB,GAAG;IAAM,GAAG,MAAM;cAClC,MAAM;IACW,CAAA;GACL,CAAA;EACb,CAAA;;;AAKV,SAAgB,QACd,SAEqC;CACrC,MAAM,aAAa,SAAS,cAAc;CAC1C,MAAM,YAAY,SAAS,aAAa;AAExC,QAAO;EACL,KAAK,EAAE,SAAS,MAAM,eAAe;GACnC,MAAM,WAAW,YAAa,KAA8B;AAE5D,OAAI,CAAC,SAAS;AACZ,cAAU;AACV;;GAGF,MAAM,WAAW,IAAI,sBAClB,YAAY;AACX,QAAI,CAAC,QAAQ,GAAI,eAAgB;AACjC,aAAS,YAAY;AACrB,cAAU;MAEZ;IAAE;IAAY;IAAW,CAC1B;AACD,YAAS,QAAQ,QAAQ;AACzB,gBAAa,SAAS,YAAY;;EAEpC,IAAI;EACL"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { condition, interaction, media } from './hydration/generic.js';
|
|
2
|
+
export { idle } from './hydration/idle.js';
|
|
3
|
+
export { load } from './hydration/load.js';
|
|
4
|
+
export { never } from './hydration/never.js';
|
|
5
|
+
export { visible } from './hydration/visible.js';
|
|
6
|
+
export type { HydrationCondition, HydrationInteractionEvent, HydrationInteractionEvents, IdleHydrationOptions, HydrationPrefetchContext, HydrationPrefetchFunction, HydrationPrefetchWhen, HydrationPrefetchStrategy, HydrationPrefetchWaitReason, HydrationStrategyTypes, HydrationWhen, VisibleHydrationOptions, } from '@tanstack/start-client-core/hydration';
|
|
7
|
+
export type { HydrationStrategy, ReactHydrationStrategy } from './Hydrate.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { condition, interaction, media } from "./hydration/generic.js";
|
|
3
|
+
import { idle } from "./hydration/idle.js";
|
|
4
|
+
import { load } from "./hydration/load.js";
|
|
5
|
+
import { never } from "./hydration/never.js";
|
|
6
|
+
import { visible } from "./hydration/visible.js";
|
|
7
|
+
export { condition, idle, interaction, load, media, never, visible };
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { StartClient } from './StartClient.js';
|
|
2
2
|
export { hydrateStart } from './hydrateStart.js';
|
|
3
|
+
export { Hydrate } from './Hydrate.js';
|
|
4
|
+
export type { HydrateOptions, HydrateProps, HydrateWhen, HydrationInteractionEvent, HydrationInteractionEvents, HydrationPrefetchContext, HydrationPrefetchFunction, HydrationPrefetchStrategy, HydrationPrefetchWaitReason, HydrationStrategy, HydrationWhen, } from './Hydrate.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-start-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.168.0",
|
|
4
4
|
"description": "Modern and scalable routing for React applications",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,12 @@
|
|
|
32
32
|
"default": "./dist/esm/index.js"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
|
+
"./hydration": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/esm/hydration.d.ts",
|
|
38
|
+
"default": "./dist/esm/hydration.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
35
41
|
"./package.json": "./package.json"
|
|
36
42
|
},
|
|
37
43
|
"sideEffects": false,
|
|
@@ -43,9 +49,9 @@
|
|
|
43
49
|
"node": ">=22.12.0"
|
|
44
50
|
},
|
|
45
51
|
"dependencies": {
|
|
46
|
-
"@tanstack/react-router": "1.170.
|
|
47
|
-
"@tanstack/router-core": "1.171.
|
|
48
|
-
"@tanstack/start-client-core": "1.
|
|
52
|
+
"@tanstack/react-router": "1.170.5",
|
|
53
|
+
"@tanstack/router-core": "1.171.3",
|
|
54
|
+
"@tanstack/start-client-core": "1.170.0"
|
|
49
55
|
},
|
|
50
56
|
"devDependencies": {
|
|
51
57
|
"@testing-library/react": "^16.2.0",
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
|
|
5
|
+
import { reactUse, useHydrated, useLayoutEffect } from '@tanstack/react-router'
|
|
6
|
+
import { isServer } from '@tanstack/router-core/isServer'
|
|
7
|
+
import {
|
|
8
|
+
hydrateIdAttribute,
|
|
9
|
+
hydrateWhenAttribute,
|
|
10
|
+
} from '@tanstack/start-client-core/hydration/constants'
|
|
11
|
+
import {
|
|
12
|
+
createResolvedGate,
|
|
13
|
+
getFallbackHtml,
|
|
14
|
+
getOrCreateGate,
|
|
15
|
+
onGateResolve,
|
|
16
|
+
releaseGate,
|
|
17
|
+
runHydrationStrategyCleanup,
|
|
18
|
+
saveFallbackHtml,
|
|
19
|
+
waitForHydrationPrefetchStrategy,
|
|
20
|
+
} from '@tanstack/start-client-core/hydration/runtime'
|
|
21
|
+
import { listenForDelegatedHydrationIntent } from '@tanstack/start-client-core/hydration'
|
|
22
|
+
import type {
|
|
23
|
+
HydrationRuntimeContext,
|
|
24
|
+
HydrationStrategy,
|
|
25
|
+
HydrationWhen,
|
|
26
|
+
} from '@tanstack/start-client-core/hydration'
|
|
27
|
+
import type { HydrationGateRecord } from '@tanstack/start-client-core/hydration/runtime'
|
|
28
|
+
import type { HydrateProps, InternalHydrateProps } from './Hydrate'
|
|
29
|
+
|
|
30
|
+
type Gate = HydrationGateRecord & { promise: Promise<void> }
|
|
31
|
+
type PrefetchController = {
|
|
32
|
+
abortController: AbortController
|
|
33
|
+
hydrationRequested: boolean
|
|
34
|
+
hydrationListeners: Set<() => void>
|
|
35
|
+
hydrationResolvePending: boolean
|
|
36
|
+
started: boolean
|
|
37
|
+
promise?: Promise<void>
|
|
38
|
+
cleanup?: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const dynamicType = 'dynamic'
|
|
42
|
+
const dynamicHydrateStrategy = {
|
|
43
|
+
_t: dynamicType,
|
|
44
|
+
_d: () => true,
|
|
45
|
+
} satisfies HydrationStrategy<typeof dynamicType, false>
|
|
46
|
+
|
|
47
|
+
function shouldDeferHydration(strategy: HydrationStrategy) {
|
|
48
|
+
return strategy._d ? strategy._d() : strategy._t !== 'load'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function useLatest<T>(value: T) {
|
|
52
|
+
const ref = React.useRef(value)
|
|
53
|
+
ref.current = value
|
|
54
|
+
return ref
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function useHydrationGate(props: InternalHydrateProps) {
|
|
58
|
+
const hydrated = useHydrated()
|
|
59
|
+
const reactId = React.useId()
|
|
60
|
+
const id = props.h ? `${props.h}${reactId}` : reactId
|
|
61
|
+
const when = props.when
|
|
62
|
+
const isDynamicHydrate = typeof when === 'function'
|
|
63
|
+
const dynamicHydrateStrategyRef = React.useRef<HydrationStrategy | undefined>(
|
|
64
|
+
undefined,
|
|
65
|
+
)
|
|
66
|
+
if (isDynamicHydrate) {
|
|
67
|
+
dynamicHydrateStrategyRef.current ??=
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
69
|
+
(isServer ?? typeof window === 'undefined')
|
|
70
|
+
? dynamicHydrateStrategy
|
|
71
|
+
: when()
|
|
72
|
+
}
|
|
73
|
+
const hydrateStrategy = isDynamicHydrate
|
|
74
|
+
? dynamicHydrateStrategyRef.current!
|
|
75
|
+
: when
|
|
76
|
+
const markerHydrateType: HydrationWhen = isDynamicHydrate
|
|
77
|
+
? dynamicType
|
|
78
|
+
: hydrateStrategy._t!
|
|
79
|
+
const [prefetchError, setPrefetchError] = React.useState<unknown>()
|
|
80
|
+
const latestRef = useLatest({
|
|
81
|
+
prefetch: props.prefetch,
|
|
82
|
+
preload: props.p,
|
|
83
|
+
})
|
|
84
|
+
const gateRef = React.useRef<HydrationGateRecord | undefined>(undefined)
|
|
85
|
+
const markerElementRef = React.useRef<HTMLDivElement | null>(null)
|
|
86
|
+
const shouldPreserveServerHTMLRef = React.useRef<boolean | undefined>(
|
|
87
|
+
undefined,
|
|
88
|
+
)
|
|
89
|
+
const shouldDeferInitialHydrationRef = React.useRef<boolean | undefined>(
|
|
90
|
+
undefined,
|
|
91
|
+
)
|
|
92
|
+
const didPrefetchRef = React.useRef(false)
|
|
93
|
+
const prefetchControllerRef = React.useRef<PrefetchController | undefined>(
|
|
94
|
+
undefined,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
prefetchControllerRef.current ??= {
|
|
98
|
+
abortController: new AbortController(),
|
|
99
|
+
hydrationRequested: false,
|
|
100
|
+
hydrationListeners: new Set<() => void>(),
|
|
101
|
+
hydrationResolvePending: false,
|
|
102
|
+
started: false,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
shouldPreserveServerHTMLRef.current ??=
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
107
|
+
(isServer ?? typeof window === 'undefined') || !hydrated
|
|
108
|
+
shouldDeferInitialHydrationRef.current ??=
|
|
109
|
+
!hydrated && shouldDeferHydration(hydrateStrategy)
|
|
110
|
+
|
|
111
|
+
if (!gateRef.current) {
|
|
112
|
+
gateRef.current =
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
114
|
+
(isServer ?? typeof window === 'undefined')
|
|
115
|
+
? createResolvedGate(id, hydrateStrategy._t!)
|
|
116
|
+
: getOrCreateGate(id, hydrateStrategy._t!)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
gateRef.current.when = hydrateStrategy._t!
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
123
|
+
!(isServer ?? typeof window === 'undefined') &&
|
|
124
|
+
hydrateStrategy._t !== 'never' &&
|
|
125
|
+
(!shouldDeferInitialHydrationRef.current ||
|
|
126
|
+
!shouldDeferHydration(hydrateStrategy))
|
|
127
|
+
) {
|
|
128
|
+
gateRef.current.resolve()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const markerRef = React.useCallback(
|
|
132
|
+
(element: HTMLDivElement | null) => {
|
|
133
|
+
markerElementRef.current = element
|
|
134
|
+
if (element) {
|
|
135
|
+
if (
|
|
136
|
+
hydrateStrategy._t === 'never' &&
|
|
137
|
+
!shouldPreserveServerHTMLRef.current
|
|
138
|
+
) {
|
|
139
|
+
element.replaceChildren()
|
|
140
|
+
}
|
|
141
|
+
saveFallbackHtml(id, element)
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
[hydrateStrategy._t, id],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
React.useEffect(() => {
|
|
148
|
+
const gate = gateRef.current!
|
|
149
|
+
return () => {
|
|
150
|
+
const controller = prefetchControllerRef.current
|
|
151
|
+
controller?.abortController.abort()
|
|
152
|
+
controller?.cleanup?.()
|
|
153
|
+
controller?.hydrationListeners.clear()
|
|
154
|
+
releaseGate(gate)
|
|
155
|
+
}
|
|
156
|
+
}, [])
|
|
157
|
+
|
|
158
|
+
React.useEffect(() => {
|
|
159
|
+
if (
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
161
|
+
(isServer ?? typeof window === 'undefined') ||
|
|
162
|
+
!latestRef.current.prefetch
|
|
163
|
+
) {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const controller = prefetchControllerRef.current!
|
|
168
|
+
if (controller.started) return
|
|
169
|
+
controller.started = true
|
|
170
|
+
|
|
171
|
+
const onHydrate = (listener: () => void) => {
|
|
172
|
+
if (controller.hydrationRequested) {
|
|
173
|
+
listener()
|
|
174
|
+
return () => {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
controller.hydrationListeners.add(listener)
|
|
178
|
+
return () => {
|
|
179
|
+
controller.hydrationListeners.delete(listener)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const preload = () => latestRef.current.preload?.() ?? Promise.resolve()
|
|
184
|
+
const prefetchInput = latestRef.current.prefetch
|
|
185
|
+
|
|
186
|
+
if (typeof prefetchInput === 'function') {
|
|
187
|
+
const promise = Promise.resolve()
|
|
188
|
+
.then(() =>
|
|
189
|
+
prefetchInput({
|
|
190
|
+
element: markerElementRef.current,
|
|
191
|
+
signal: controller.abortController.signal,
|
|
192
|
+
preload,
|
|
193
|
+
waitFor: (strategy) =>
|
|
194
|
+
waitForHydrationPrefetchStrategy(strategy, {
|
|
195
|
+
element: markerElementRef.current,
|
|
196
|
+
signal: controller.abortController.signal,
|
|
197
|
+
onHydrate,
|
|
198
|
+
}),
|
|
199
|
+
}),
|
|
200
|
+
)
|
|
201
|
+
.then(() => undefined)
|
|
202
|
+
|
|
203
|
+
controller.promise = promise
|
|
204
|
+
promise.catch((error) => {
|
|
205
|
+
if (!controller.abortController.signal.aborted) {
|
|
206
|
+
setPrefetchError(error)
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!latestRef.current.preload) return
|
|
213
|
+
|
|
214
|
+
const prefetch = () => {
|
|
215
|
+
if (didPrefetchRef.current) return
|
|
216
|
+
didPrefetchRef.current = true
|
|
217
|
+
void preload()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
controller.cleanup = runHydrationStrategyCleanup(
|
|
221
|
+
prefetchInput._s?.({
|
|
222
|
+
element: markerElementRef.current,
|
|
223
|
+
prefetch,
|
|
224
|
+
}),
|
|
225
|
+
)
|
|
226
|
+
}, [hydrateStrategy, latestRef])
|
|
227
|
+
|
|
228
|
+
useLayoutEffect(() => {
|
|
229
|
+
const gate = gateRef.current!
|
|
230
|
+
if (
|
|
231
|
+
!shouldDeferInitialHydrationRef.current ||
|
|
232
|
+
hydrateStrategy._t === 'never'
|
|
233
|
+
) {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (gate.resolved) {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const cleanups: Array<() => void> = []
|
|
242
|
+
let removeResolveListener = () => {}
|
|
243
|
+
let disposed = false
|
|
244
|
+
const resolveGate = gate.resolve
|
|
245
|
+
|
|
246
|
+
const cleanup = () => {
|
|
247
|
+
if (disposed) return
|
|
248
|
+
disposed = true
|
|
249
|
+
if (gate.resolve === requestHydration) {
|
|
250
|
+
gate.resolve = resolveGate
|
|
251
|
+
}
|
|
252
|
+
removeResolveListener()
|
|
253
|
+
cleanups.forEach((fn) => fn())
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const addCleanup = (fn: void | (() => void)) => {
|
|
257
|
+
if (!fn) return
|
|
258
|
+
if (disposed || gate.resolved) {
|
|
259
|
+
fn()
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
cleanups.push(fn)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const requestHydration = () => {
|
|
266
|
+
const controller = prefetchControllerRef.current!
|
|
267
|
+
if (!controller.hydrationRequested) {
|
|
268
|
+
controller.hydrationRequested = true
|
|
269
|
+
controller.hydrationListeners.forEach((listener) => listener())
|
|
270
|
+
controller.hydrationListeners.clear()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!controller.promise) {
|
|
274
|
+
resolveGate()
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
if (controller.hydrationResolvePending) return
|
|
278
|
+
controller.hydrationResolvePending = true
|
|
279
|
+
|
|
280
|
+
controller.promise.then(
|
|
281
|
+
() => resolveGate(),
|
|
282
|
+
(error) => {
|
|
283
|
+
if (!controller.abortController.signal.aborted) {
|
|
284
|
+
setPrefetchError(error)
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
gate.resolve = requestHydration
|
|
291
|
+
removeResolveListener = onGateResolve(gate, cleanup)
|
|
292
|
+
|
|
293
|
+
const context: HydrationRuntimeContext = {
|
|
294
|
+
element: markerElementRef.current,
|
|
295
|
+
gate,
|
|
296
|
+
}
|
|
297
|
+
addCleanup(runHydrationStrategyCleanup(hydrateStrategy._s?.(context)))
|
|
298
|
+
|
|
299
|
+
if (hydrateStrategy._t !== 'interaction') {
|
|
300
|
+
addCleanup(
|
|
301
|
+
runHydrationStrategyCleanup(
|
|
302
|
+
markerElementRef.current
|
|
303
|
+
? listenForDelegatedHydrationIntent(
|
|
304
|
+
markerElementRef.current,
|
|
305
|
+
context,
|
|
306
|
+
)
|
|
307
|
+
: undefined,
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return cleanup
|
|
313
|
+
}, [hydrateStrategy, latestRef])
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
gate: gateRef.current,
|
|
317
|
+
markerRef,
|
|
318
|
+
markerElementRef,
|
|
319
|
+
hydrateStrategy,
|
|
320
|
+
markerHydrateType,
|
|
321
|
+
prefetchError,
|
|
322
|
+
shouldPreserveServerHTML: shouldPreserveServerHTMLRef.current,
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function HydrationGate(props: { gate: Gate; children: React.ReactNode }) {
|
|
327
|
+
if (
|
|
328
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
329
|
+
isServer ??
|
|
330
|
+
typeof window === 'undefined'
|
|
331
|
+
) {
|
|
332
|
+
return props.children as React.JSX.Element
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (props.gate.resolved) {
|
|
336
|
+
return props.children as React.JSX.Element
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!reactUse) {
|
|
340
|
+
throw props.gate.promise
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
reactUse(props.gate.promise)
|
|
344
|
+
|
|
345
|
+
return props.children as React.JSX.Element
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function HydratedBoundary(props: {
|
|
349
|
+
id: string
|
|
350
|
+
onHydrated?: () => void
|
|
351
|
+
onStrategyHydrated?: (id: string) => void
|
|
352
|
+
children: React.ReactNode
|
|
353
|
+
}) {
|
|
354
|
+
const { id, onHydrated, onStrategyHydrated } = props
|
|
355
|
+
const didHydrateRef = React.useRef(false)
|
|
356
|
+
|
|
357
|
+
React.useEffect(() => {
|
|
358
|
+
if (didHydrateRef.current) return
|
|
359
|
+
didHydrateRef.current = true
|
|
360
|
+
onHydrated?.()
|
|
361
|
+
onStrategyHydrated?.(id)
|
|
362
|
+
}, [id, onHydrated, onStrategyHydrated])
|
|
363
|
+
|
|
364
|
+
return props.children as React.JSX.Element
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function GenericHydrate(props: HydrateProps): React.JSX.Element {
|
|
368
|
+
const internalProps = props as InternalHydrateProps
|
|
369
|
+
const {
|
|
370
|
+
gate,
|
|
371
|
+
hydrateStrategy,
|
|
372
|
+
markerHydrateType,
|
|
373
|
+
markerElementRef,
|
|
374
|
+
markerRef,
|
|
375
|
+
prefetchError,
|
|
376
|
+
shouldPreserveServerHTML,
|
|
377
|
+
} = useHydrationGate(internalProps)
|
|
378
|
+
if (prefetchError) throw prefetchError
|
|
379
|
+
|
|
380
|
+
const fallback = shouldPreserveServerHTML
|
|
381
|
+
? (() => {
|
|
382
|
+
const html = getFallbackHtml(gate.id)
|
|
383
|
+
return html ? (
|
|
384
|
+
<div
|
|
385
|
+
style={{ display: 'contents' }}
|
|
386
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
387
|
+
/>
|
|
388
|
+
) : null
|
|
389
|
+
})()
|
|
390
|
+
: (props.fallback ?? null)
|
|
391
|
+
const markerAttributes =
|
|
392
|
+
markerHydrateType === dynamicType ? undefined : hydrateStrategy._a?.()
|
|
393
|
+
|
|
394
|
+
const hydrateType = hydrateStrategy._t!
|
|
395
|
+
|
|
396
|
+
if (hydrateType === 'never' && !shouldPreserveServerHTML) {
|
|
397
|
+
return (
|
|
398
|
+
<div
|
|
399
|
+
ref={markerRef}
|
|
400
|
+
{...{
|
|
401
|
+
[hydrateIdAttribute]: gate.id,
|
|
402
|
+
[hydrateWhenAttribute]: markerHydrateType,
|
|
403
|
+
...markerAttributes,
|
|
404
|
+
}}
|
|
405
|
+
>
|
|
406
|
+
{props.fallback ?? null}
|
|
407
|
+
</div>
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<div
|
|
413
|
+
ref={markerRef}
|
|
414
|
+
{...{
|
|
415
|
+
[hydrateIdAttribute]: gate.id,
|
|
416
|
+
[hydrateWhenAttribute]: markerHydrateType,
|
|
417
|
+
...markerAttributes,
|
|
418
|
+
}}
|
|
419
|
+
>
|
|
420
|
+
<React.Suspense fallback={fallback}>
|
|
421
|
+
<HydrationGate gate={gate}>
|
|
422
|
+
<HydratedBoundary
|
|
423
|
+
id={gate.id}
|
|
424
|
+
onHydrated={props.onHydrated}
|
|
425
|
+
onStrategyHydrated={(id) => {
|
|
426
|
+
markerElementRef.current?.removeAttribute(hydrateWhenAttribute)
|
|
427
|
+
hydrateStrategy._o?.(id)
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
{props.children}
|
|
431
|
+
</HydratedBoundary>
|
|
432
|
+
</HydrationGate>
|
|
433
|
+
</React.Suspense>
|
|
434
|
+
</div>
|
|
435
|
+
)
|
|
436
|
+
}
|
package/src/Hydrate.tsx
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
|
|
5
|
+
import { isServer } from '@tanstack/router-core/isServer'
|
|
6
|
+
import type {
|
|
7
|
+
HydrationStrategy as CoreHydrationStrategy,
|
|
8
|
+
HydrationPrefetchFunction,
|
|
9
|
+
HydrationPrefetchStrategy,
|
|
10
|
+
HydrationWhen,
|
|
11
|
+
} from '@tanstack/start-client-core/hydration'
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
HydrationInteractionEvent,
|
|
15
|
+
HydrationInteractionEvents,
|
|
16
|
+
HydrationPrefetchContext,
|
|
17
|
+
HydrationPrefetchFunction,
|
|
18
|
+
HydrationPrefetchStrategy,
|
|
19
|
+
HydrationPrefetchWaitReason,
|
|
20
|
+
HydrationWhen,
|
|
21
|
+
} from '@tanstack/start-client-core/hydration'
|
|
22
|
+
|
|
23
|
+
export type ReactHydrationStrategy<
|
|
24
|
+
TWhen extends HydrationWhen = HydrationWhen,
|
|
25
|
+
TCanPrefetch extends boolean = boolean,
|
|
26
|
+
> = CoreHydrationStrategy<TWhen, TCanPrefetch> & {
|
|
27
|
+
_h: (this: ReactHydrationStrategy, props: HydrateProps) => React.JSX.Element
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type HydrationStrategy<
|
|
31
|
+
TWhen extends HydrationWhen = HydrationWhen,
|
|
32
|
+
TCanPrefetch extends boolean = boolean,
|
|
33
|
+
> = ReactHydrationStrategy<TWhen, TCanPrefetch>
|
|
34
|
+
|
|
35
|
+
export type HydrateWhen =
|
|
36
|
+
| ReactHydrationStrategy
|
|
37
|
+
| (() => ReactHydrationStrategy)
|
|
38
|
+
|
|
39
|
+
type HydrateCommonOptions = {
|
|
40
|
+
when: HydrateWhen
|
|
41
|
+
fallback?: React.ReactNode
|
|
42
|
+
onHydrated?: () => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type HydrateOptions =
|
|
46
|
+
| (HydrateCommonOptions & {
|
|
47
|
+
prefetch?: never
|
|
48
|
+
split?: boolean
|
|
49
|
+
})
|
|
50
|
+
| (HydrateCommonOptions & {
|
|
51
|
+
prefetch: HydrationPrefetchStrategy
|
|
52
|
+
split?: true
|
|
53
|
+
})
|
|
54
|
+
| (HydrateCommonOptions & {
|
|
55
|
+
prefetch: HydrationPrefetchFunction
|
|
56
|
+
split?: boolean
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export type HydrateProps = HydrateOptions & {
|
|
60
|
+
children: React.ReactNode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type InternalHydrateProps = HydrateProps & {
|
|
64
|
+
h?: string
|
|
65
|
+
p?: () => Promise<void>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const dynamicType = 'dynamic'
|
|
69
|
+
const hydrateIdAttribute = 'data-ts-hydrate-id'
|
|
70
|
+
const hydrateWhenAttribute = 'data-ts-hydrate-when'
|
|
71
|
+
|
|
72
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
73
|
+
function ServerDynamicHydrate(props: HydrateProps): React.JSX.Element {
|
|
74
|
+
const internalProps = props as InternalHydrateProps
|
|
75
|
+
const reactId = React.useId()
|
|
76
|
+
const id = internalProps.h ? `${internalProps.h}${reactId}` : reactId
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
{...{
|
|
81
|
+
[hydrateIdAttribute]: id,
|
|
82
|
+
[hydrateWhenAttribute]: dynamicType,
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<React.Suspense fallback={props.fallback ?? null}>
|
|
86
|
+
{props.children}
|
|
87
|
+
</React.Suspense>
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
93
|
+
export function Hydrate(props: HydrateProps): React.JSX.Element {
|
|
94
|
+
if (typeof props.when === 'function') {
|
|
95
|
+
if (
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
97
|
+
isServer ??
|
|
98
|
+
typeof window === 'undefined'
|
|
99
|
+
) {
|
|
100
|
+
return <ServerDynamicHydrate {...props} />
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return props.when()._h(props)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return props.when._h(props)
|
|
107
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
condition as coreCondition,
|
|
5
|
+
interaction as coreInteraction,
|
|
6
|
+
media as coreMedia,
|
|
7
|
+
withHydrationRenderer,
|
|
8
|
+
} from '@tanstack/start-client-core/hydration'
|
|
9
|
+
import { GenericHydrate } from '../GenericHydrate'
|
|
10
|
+
import type {
|
|
11
|
+
HydrationCondition,
|
|
12
|
+
HydrationInteractionEvents,
|
|
13
|
+
HydrationPrefetchStrategy,
|
|
14
|
+
} from '@tanstack/start-client-core/hydration'
|
|
15
|
+
import type { ReactHydrationStrategy } from '../Hydrate'
|
|
16
|
+
|
|
17
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
18
|
+
export function media(
|
|
19
|
+
query: string,
|
|
20
|
+
): ReactHydrationStrategy<'media', true> & HydrationPrefetchStrategy<'media'> {
|
|
21
|
+
return /* @__PURE__ */ withHydrationRenderer(coreMedia(query), GenericHydrate)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
+
export function condition(
|
|
26
|
+
condition: HydrationCondition,
|
|
27
|
+
): ReactHydrationStrategy<'condition', false> {
|
|
28
|
+
return /* @__PURE__ */ withHydrationRenderer(
|
|
29
|
+
coreCondition(condition),
|
|
30
|
+
GenericHydrate,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
35
|
+
export function interaction(options?: {
|
|
36
|
+
events?: HydrationInteractionEvents
|
|
37
|
+
}): ReactHydrationStrategy<'interaction', true> &
|
|
38
|
+
HydrationPrefetchStrategy<'interaction'> {
|
|
39
|
+
return /* @__PURE__ */ withHydrationRenderer(
|
|
40
|
+
coreInteraction(options),
|
|
41
|
+
GenericHydrate,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
idle as coreIdle,
|
|
5
|
+
withHydrationRenderer,
|
|
6
|
+
} from '@tanstack/start-client-core/hydration'
|
|
7
|
+
import { GenericHydrate } from '../GenericHydrate'
|
|
8
|
+
import type {
|
|
9
|
+
HydrationPrefetchStrategy,
|
|
10
|
+
IdleHydrationOptions,
|
|
11
|
+
} from '@tanstack/start-client-core/hydration'
|
|
12
|
+
import type { ReactHydrationStrategy } from '../Hydrate'
|
|
13
|
+
|
|
14
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
15
|
+
export function idle(
|
|
16
|
+
options: IdleHydrationOptions = {},
|
|
17
|
+
): ReactHydrationStrategy<'idle', true> & HydrationPrefetchStrategy<'idle'> {
|
|
18
|
+
return /* @__PURE__ */ withHydrationRenderer(
|
|
19
|
+
coreIdle(options),
|
|
20
|
+
GenericHydrate,
|
|
21
|
+
)
|
|
22
|
+
}
|