@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.
Files changed (39) hide show
  1. package/dist/esm/GenericHydrate.d.ts +3 -0
  2. package/dist/esm/GenericHydrate.js +243 -0
  3. package/dist/esm/GenericHydrate.js.map +1 -0
  4. package/dist/esm/Hydrate.d.ts +31 -0
  5. package/dist/esm/Hydrate.js +34 -0
  6. package/dist/esm/Hydrate.js.map +1 -0
  7. package/dist/esm/hydration/generic.d.ts +7 -0
  8. package/dist/esm/hydration/generic.js +20 -0
  9. package/dist/esm/hydration/generic.js.map +1 -0
  10. package/dist/esm/hydration/idle.d.ts +3 -0
  11. package/dist/esm/hydration/idle.js +12 -0
  12. package/dist/esm/hydration/idle.js.map +1 -0
  13. package/dist/esm/hydration/load.d.ts +5 -0
  14. package/dist/esm/hydration/load.js +33 -0
  15. package/dist/esm/hydration/load.js.map +1 -0
  16. package/dist/esm/hydration/never.d.ts +4 -0
  17. package/dist/esm/hydration/never.js +56 -0
  18. package/dist/esm/hydration/never.js.map +1 -0
  19. package/dist/esm/hydration/visible.d.ts +5 -0
  20. package/dist/esm/hydration/visible.js +94 -0
  21. package/dist/esm/hydration/visible.js.map +1 -0
  22. package/dist/esm/hydration.d.ts +7 -0
  23. package/dist/esm/hydration.js +7 -0
  24. package/dist/esm/index.d.ts +2 -0
  25. package/dist/esm/index.js +3 -1
  26. package/dist/esm/tests/Hydrate.test-d.d.ts +1 -0
  27. package/dist/esm/tests/Hydrate.test.d.ts +1 -0
  28. package/package.json +10 -4
  29. package/src/GenericHydrate.tsx +436 -0
  30. package/src/Hydrate.tsx +107 -0
  31. package/src/hydration/generic.ts +43 -0
  32. package/src/hydration/idle.ts +22 -0
  33. package/src/hydration/load.tsx +49 -0
  34. package/src/hydration/never.tsx +97 -0
  35. package/src/hydration/visible.tsx +139 -0
  36. package/src/hydration.ts +22 -0
  37. package/src/index.tsx +16 -0
  38. package/src/tests/Hydrate.test-d.tsx +147 -0
  39. package/src/tests/Hydrate.test.tsx +676 -0
@@ -0,0 +1,49 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import {
6
+ load as coreLoad,
7
+ withHydrationRenderer,
8
+ } from '@tanstack/start-client-core/hydration'
9
+ import type { HydrationPrefetchStrategy } from '@tanstack/start-client-core/hydration'
10
+ import type { HydrateProps, ReactHydrationStrategy } from '../Hydrate'
11
+
12
+ function HydratedBoundary(props: {
13
+ onHydrated?: () => void
14
+ children: React.ReactNode
15
+ }) {
16
+ const { onHydrated, children } = props
17
+ const didHydrateRef = React.useRef(false)
18
+
19
+ React.useEffect(() => {
20
+ if (didHydrateRef.current) return
21
+ didHydrateRef.current = true
22
+ onHydrated?.()
23
+ }, [onHydrated])
24
+
25
+ return children as React.JSX.Element
26
+ }
27
+
28
+ export function LoadHydrate(props: HydrateProps): React.JSX.Element {
29
+ return (
30
+ <div>
31
+ <React.Suspense fallback={props.fallback ?? null}>
32
+ <HydratedBoundary onHydrated={props.onHydrated}>
33
+ {props.children}
34
+ </HydratedBoundary>
35
+ </React.Suspense>
36
+ </div>
37
+ )
38
+ }
39
+
40
+ const loadStrategy = /* @__PURE__ */ withHydrationRenderer(
41
+ coreLoad(),
42
+ LoadHydrate,
43
+ ) as ReactHydrationStrategy<'load', true> & HydrationPrefetchStrategy<'load'>
44
+
45
+ /* @__NO_SIDE_EFFECTS__ */
46
+ export function load(): ReactHydrationStrategy<'load', true> &
47
+ HydrationPrefetchStrategy<'load'> {
48
+ return loadStrategy
49
+ }
@@ -0,0 +1,97 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import { reactUse, useHydrated } from '@tanstack/react-router'
6
+ import { isServer } from '@tanstack/router-core/isServer'
7
+ import {
8
+ never as coreNever,
9
+ withHydrationRenderer,
10
+ } from '@tanstack/start-client-core/hydration'
11
+ import {
12
+ hydrateIdAttribute,
13
+ hydrateWhenAttribute,
14
+ } from '@tanstack/start-client-core/hydration/constants'
15
+ import {
16
+ getFallbackHtml,
17
+ saveFallbackHtml,
18
+ } from '@tanstack/start-client-core/hydration/runtime'
19
+ import type {
20
+ HydrateProps,
21
+ InternalHydrateProps,
22
+ ReactHydrationStrategy,
23
+ } from '../Hydrate'
24
+
25
+ const neverType = 'never'
26
+ const neverPromise = new Promise<void>(() => {})
27
+
28
+ function NeverGate(props: { children: React.ReactNode }) {
29
+ if (
30
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
31
+ isServer ??
32
+ typeof window === 'undefined'
33
+ ) {
34
+ return props.children as React.JSX.Element
35
+ }
36
+
37
+ if (!reactUse) {
38
+ throw neverPromise
39
+ }
40
+
41
+ reactUse(neverPromise)
42
+
43
+ return props.children as React.JSX.Element
44
+ }
45
+
46
+ export function NeverHydrate(props: HydrateProps): React.JSX.Element {
47
+ const internalProps = props as InternalHydrateProps
48
+ const hydrated = useHydrated()
49
+ const reactId = React.useId()
50
+ const id = internalProps.h ? `${internalProps.h}${reactId}` : reactId
51
+ const shouldPreserveServerHTMLRef = React.useRef<boolean | undefined>(
52
+ undefined,
53
+ )
54
+ shouldPreserveServerHTMLRef.current ??=
55
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
56
+ (isServer ?? typeof window === 'undefined') || !hydrated
57
+ const markerRef = React.useCallback(
58
+ (element: HTMLDivElement | null) => {
59
+ if (!element) return
60
+ if (!shouldPreserveServerHTMLRef.current) {
61
+ element.replaceChildren()
62
+ } else {
63
+ saveFallbackHtml(id, element)
64
+ }
65
+ },
66
+ [id],
67
+ )
68
+ const markerProps = {
69
+ ref: markerRef,
70
+ [hydrateIdAttribute]: id,
71
+ [hydrateWhenAttribute]: neverType,
72
+ }
73
+ const fallback = (() => {
74
+ const html = getFallbackHtml(id)
75
+ return html ? (
76
+ <div
77
+ style={{ display: 'contents' }}
78
+ dangerouslySetInnerHTML={{ __html: html }}
79
+ />
80
+ ) : (
81
+ (props.fallback ?? null)
82
+ )
83
+ })()
84
+
85
+ return (
86
+ <div {...markerProps}>
87
+ <React.Suspense fallback={fallback}>
88
+ <NeverGate>{props.children}</NeverGate>
89
+ </React.Suspense>
90
+ </div>
91
+ )
92
+ }
93
+
94
+ /* @__NO_SIDE_EFFECTS__ */
95
+ export function never(): ReactHydrationStrategy<'never', false> {
96
+ return /* @__PURE__ */ withHydrationRenderer(coreNever(), NeverHydrate)
97
+ }
@@ -0,0 +1,139 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import { reactUse } from '@tanstack/react-router'
6
+ import { isServer } from '@tanstack/router-core/isServer'
7
+ import type {
8
+ HydrationPrefetchStrategy,
9
+ VisibleHydrationOptions,
10
+ } from '@tanstack/start-client-core/hydration'
11
+ import type {
12
+ HydrateProps,
13
+ InternalHydrateProps,
14
+ ReactHydrationStrategy,
15
+ } from '../Hydrate'
16
+
17
+ type VisibleGate = {
18
+ p: Promise<void>
19
+ r: boolean
20
+ s: () => void
21
+ }
22
+
23
+ /* @__NO_SIDE_EFFECTS__ */
24
+ function HydrationBoundary(props: {
25
+ g: VisibleGate
26
+ o?: () => void
27
+ children?: React.ReactNode
28
+ }) {
29
+ const { g, o } = props
30
+
31
+ if (!g.r) {
32
+ if (!reactUse) {
33
+ throw g.p
34
+ }
35
+
36
+ reactUse(g.p)
37
+ }
38
+
39
+ React.useEffect(() => {
40
+ o?.()
41
+ }, [o])
42
+
43
+ return props.children as React.JSX.Element
44
+ }
45
+
46
+ /* @__NO_SIDE_EFFECTS__ */
47
+ export function VisibleHydrate(
48
+ this: ReactHydrationStrategy,
49
+ props: HydrateProps,
50
+ ): React.JSX.Element {
51
+ const strategy = this as ReactHydrationStrategy<'visible', true>
52
+ const prefetchStrategy = props.prefetch
53
+ const preload = (props as InternalHydrateProps).p
54
+ const markerRef = React.useRef<HTMLDivElement | null>(null)
55
+ const [gate] = React.useState<VisibleGate>(() => {
56
+ let resolvePromise!: () => void
57
+ const nextGate: VisibleGate = {
58
+ p: new Promise<void>((resolve) => {
59
+ resolvePromise = resolve
60
+ }),
61
+ r: false,
62
+ s: () => {
63
+ nextGate.r = true
64
+ resolvePromise()
65
+ },
66
+ }
67
+ if (
68
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
69
+ isServer ??
70
+ typeof window === 'undefined'
71
+ ) {
72
+ nextGate.s()
73
+ }
74
+
75
+ return nextGate
76
+ })
77
+
78
+ React.useEffect(() => {
79
+ if (!preload || typeof prefetchStrategy === 'function') {
80
+ return
81
+ }
82
+
83
+ return prefetchStrategy?._s?.({
84
+ element: markerRef.current,
85
+ prefetch: preload,
86
+ })
87
+ }, [prefetchStrategy, preload])
88
+
89
+ React.useEffect(() => {
90
+ if (gate.r) return
91
+
92
+ return strategy._s?.({
93
+ element: markerRef.current,
94
+ gate: gate as never,
95
+ })
96
+ }, [gate, strategy])
97
+
98
+ return (
99
+ <div ref={markerRef}>
100
+ <React.Suspense fallback={props.fallback}>
101
+ <HydrationBoundary g={gate} o={props.onHydrated}>
102
+ {props.children}
103
+ </HydrationBoundary>
104
+ </React.Suspense>
105
+ </div>
106
+ )
107
+ }
108
+
109
+ /* @__NO_SIDE_EFFECTS__ */
110
+ export function visible(
111
+ options?: VisibleHydrationOptions,
112
+ ): ReactHydrationStrategy<'visible', true> &
113
+ HydrationPrefetchStrategy<'visible'> {
114
+ const rootMargin = options?.rootMargin ?? '600px'
115
+ const threshold = options?.threshold ?? 0
116
+
117
+ return {
118
+ _s: ({ element, gate, prefetch }) => {
119
+ const callback = prefetch || (gate as never as VisibleGate).s
120
+
121
+ if (!element) {
122
+ callback()
123
+ return
124
+ }
125
+
126
+ const observer = new IntersectionObserver(
127
+ (entries) => {
128
+ if (!entries[0]!.isIntersecting) return
129
+ observer.disconnect()
130
+ callback()
131
+ },
132
+ { rootMargin, threshold },
133
+ )
134
+ observer.observe(element)
135
+ return () => observer.disconnect()
136
+ },
137
+ _h: VisibleHydrate,
138
+ }
139
+ }
@@ -0,0 +1,22 @@
1
+ 'use client'
2
+
3
+ export { condition, interaction, media } from './hydration/generic'
4
+ export { idle } from './hydration/idle'
5
+ export { load } from './hydration/load'
6
+ export { never } from './hydration/never'
7
+ export { visible } from './hydration/visible'
8
+ export type {
9
+ HydrationCondition,
10
+ HydrationInteractionEvent,
11
+ HydrationInteractionEvents,
12
+ IdleHydrationOptions,
13
+ HydrationPrefetchContext,
14
+ HydrationPrefetchFunction,
15
+ HydrationPrefetchWhen,
16
+ HydrationPrefetchStrategy,
17
+ HydrationPrefetchWaitReason,
18
+ HydrationStrategyTypes,
19
+ HydrationWhen,
20
+ VisibleHydrationOptions,
21
+ } from '@tanstack/start-client-core/hydration'
22
+ export type { HydrationStrategy, ReactHydrationStrategy } from './Hydrate'
package/src/index.tsx CHANGED
@@ -1,2 +1,18 @@
1
+ 'use client'
2
+
1
3
  export { StartClient } from './StartClient'
2
4
  export { hydrateStart } from './hydrateStart'
5
+ export { Hydrate } from './Hydrate'
6
+ export type {
7
+ HydrateOptions,
8
+ HydrateProps,
9
+ HydrateWhen,
10
+ HydrationInteractionEvent,
11
+ HydrationInteractionEvents,
12
+ HydrationPrefetchContext,
13
+ HydrationPrefetchFunction,
14
+ HydrationPrefetchStrategy,
15
+ HydrationPrefetchWaitReason,
16
+ HydrationStrategy,
17
+ HydrationWhen,
18
+ } from './Hydrate'
@@ -0,0 +1,147 @@
1
+ import { expectTypeOf, test } from 'vitest'
2
+ import { visible } from '../hydration'
3
+ import { Hydrate } from '../Hydrate'
4
+ import type {
5
+ HydrateOptions,
6
+ HydrateProps,
7
+ HydrationPrefetchFunction,
8
+ HydrationPrefetchStrategy,
9
+ HydrationStrategy,
10
+ } from '../Hydrate'
11
+ import type { HydrationStrategy as CoreHydrationStrategy } from '@tanstack/start-client-core/hydration'
12
+ import type { ReactNode } from 'react'
13
+
14
+ type CommonHydrateProps = {
15
+ fallback?: ReactNode
16
+ onHydrated?: () => void
17
+ children: ReactNode
18
+ }
19
+
20
+ type SplitHydrateProps = CommonHydrateProps & {
21
+ when: HydrationStrategy | (() => HydrationStrategy)
22
+ prefetch?: never
23
+ split?: boolean
24
+ }
25
+
26
+ type PrefetchHydrateProps = CommonHydrateProps & {
27
+ when: HydrationStrategy | (() => HydrationStrategy)
28
+ prefetch: HydrationPrefetchStrategy
29
+ split?: true
30
+ }
31
+
32
+ type FunctionPrefetchHydrateProps = CommonHydrateProps & {
33
+ when: HydrationStrategy | (() => HydrationStrategy)
34
+ prefetch: HydrationPrefetchFunction
35
+ split?: boolean
36
+ }
37
+
38
+ test('Hydrate component accepts the public HydrateProps type', () => {
39
+ expectTypeOf(Hydrate).toBeFunction()
40
+ expectTypeOf(Hydrate).parameter(0).branded.toEqualTypeOf<HydrateProps>()
41
+ })
42
+
43
+ test('HydrateOptions supports reusable spread props', () => {
44
+ const belowFoldProps = {
45
+ when: () => visible({ rootMargin: '800px' }),
46
+ } satisfies HydrateOptions
47
+
48
+ expectTypeOf(belowFoldProps).toMatchTypeOf<HydrateOptions>()
49
+
50
+ const withFunctionPrefetch = {
51
+ when: visible(),
52
+ split: false,
53
+ prefetch: (ctx) => {
54
+ expectTypeOf(ctx.element).toEqualTypeOf<Element | null>()
55
+ expectTypeOf(ctx.signal).toEqualTypeOf<AbortSignal>()
56
+ expectTypeOf(ctx.preload).returns.toEqualTypeOf<Promise<void>>()
57
+ expectTypeOf(ctx.waitFor).returns.toEqualTypeOf<
58
+ Promise<'prefetch' | 'hydrate' | 'abort'>
59
+ >()
60
+ },
61
+ } satisfies HydrateOptions
62
+
63
+ expectTypeOf(withFunctionPrefetch).toMatchTypeOf<HydrateOptions>()
64
+ })
65
+
66
+ test('Hydrate props are exact for strategy and prefetch forms', () => {
67
+ expectTypeOf<
68
+ Extract<HydrateProps, { prefetch?: never }>
69
+ >().branded.toEqualTypeOf<SplitHydrateProps>()
70
+ expectTypeOf<
71
+ Extract<HydrateProps, { prefetch: HydrationPrefetchStrategy }>
72
+ >().branded.toEqualTypeOf<PrefetchHydrateProps>()
73
+ expectTypeOf<
74
+ Extract<HydrateProps, { prefetch: HydrationPrefetchFunction }>
75
+ >().branded.toEqualTypeOf<FunctionPrefetchHydrateProps>()
76
+ })
77
+
78
+ test('Hydrate requires a strategy', () => {
79
+ expectTypeOf<{
80
+ when: HydrationStrategy
81
+ children: ReactNode
82
+ }>().toMatchTypeOf<HydrateProps>()
83
+
84
+ expectTypeOf<{
85
+ when: () => HydrationStrategy
86
+ children: ReactNode
87
+ }>().toMatchTypeOf<HydrateProps>()
88
+
89
+ expectTypeOf<{
90
+ children: ReactNode
91
+ }>().not.toMatchTypeOf<HydrateProps>()
92
+
93
+ expectTypeOf<{
94
+ when: () => true
95
+ children: ReactNode
96
+ }>().not.toMatchTypeOf<HydrateProps>()
97
+
98
+ expectTypeOf<{
99
+ when: false
100
+ children: ReactNode
101
+ }>().not.toMatchTypeOf<HydrateProps>()
102
+ })
103
+
104
+ test('Hydrate requires a framework-renderable strategy', () => {
105
+ expectTypeOf<CoreHydrationStrategy>().not.toMatchTypeOf<HydrationStrategy>()
106
+ expectTypeOf<ReturnType<typeof visible>>().toMatchTypeOf<HydrationStrategy>()
107
+
108
+ expectTypeOf<{
109
+ when: CoreHydrationStrategy
110
+ children: ReactNode
111
+ }>().not.toMatchTypeOf<HydrateProps>()
112
+ })
113
+
114
+ test('Hydrate enforces prefetch only with split boundaries', () => {
115
+ expectTypeOf<{
116
+ when: HydrationStrategy
117
+ prefetch: HydrationPrefetchStrategy
118
+ children: ReactNode
119
+ }>().toMatchTypeOf<HydrateProps>()
120
+
121
+ expectTypeOf<{
122
+ when: HydrationStrategy
123
+ prefetch: HydrationPrefetchStrategy
124
+ split: true
125
+ children: ReactNode
126
+ }>().toMatchTypeOf<HydrateProps>()
127
+
128
+ expectTypeOf<{
129
+ when: HydrationStrategy
130
+ prefetch: HydrationPrefetchStrategy
131
+ split: false
132
+ children: ReactNode
133
+ }>().not.toMatchTypeOf<HydrateProps>()
134
+
135
+ expectTypeOf<{
136
+ when: HydrationStrategy
137
+ prefetch: HydrationPrefetchFunction
138
+ split: false
139
+ children: ReactNode
140
+ }>().toMatchTypeOf<HydrateProps>()
141
+
142
+ expectTypeOf<{
143
+ when: HydrationStrategy
144
+ prefetch: HydrationPrefetchFunction
145
+ children: ReactNode
146
+ }>().toMatchTypeOf<HydrateProps>()
147
+ })